diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f02f7838..b78a30eb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,7 +107,8 @@ jobs: - name: Build and run custom-game setup regressions run: | - scons -j$(nproc) release=1 server=0 custom-setup-test + scons -j$(nproc) release=1 server=0 custom-setup-test map-generator-defaults-test + timeout 60s ./build/src/MapGeneratorDefaultsTest glob2-map-defaults-ci 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/data/texts.ar.txt b/data/texts.ar.txt index 6b0a50ae3..a7496d3ef 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -607,9 +607,9 @@ meta-%0 [ok] ﻧﻌﻢ [old islands terrain] -ﺟُﺰﺭ ﻋﺸﻮﺍﺋﻴﺔ ﺑﻨﻤﻂ ﻗﺪﻳﻢ +الأرخبيل الوعر [old random terrain] -ﺗﻀﺎﺭﻳﺲ ﻋﺸﻮﺍﺋﻴﺔ ﺑﻨﻤﻂ ﻗﺪﻳﻢ +الساحل المتشظي [on press] ﻋﻨﺪ ﺍﻟﻀﻐﻂ [On the spot] @@ -1361,9 +1361,9 @@ Cortex [No AI orders] لا توجد أوامر الذكاء الاصطناعي [Old islands] -الجزر القديمة +الأرخبيل الوعر [Old random] -عشوائية قديمة +الساحل المتشظي [Open at least one colony to start a match.] افتح مستعمرة واحدة على الأقل لبدء المباراة. [Open book] @@ -1764,3 +1764,9 @@ OpenGL غير متوفر في هذا الإصدار. عرض الطارة تلقائيًا [settings Automatically show the torus overview while moving around the map (OpenGL).] إظهار نظرة عامة للخريطة على سطح طارة تلقائيًا أثناء التنقل في الخريطة (OpenGL). +[Channel width] +عرض القناة +[Land bridge width] +عرض الجسر البري +[Lake size] +حجم البحيرة diff --git a/data/texts.br.txt b/data/texts.br.txt index ee9f9ff15..c6d1c4fea 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -603,9 +603,9 @@ os quais [ok] Ok [old islands terrain] -velhas ilhas aleatórias +Arquipélago Acidentado [old random terrain] -velho terreno aleatório +Costa Fragmentada [on press] Ao apertar [On the spot] @@ -1359,9 +1359,9 @@ Médio - Experimental [No AI orders] Sem ordens da IA [Old islands] -Ilhas (gerador antigo) +Arquipélago Acidentado [Old random] -Aleatório (gerador antigo) +Costa Fragmentada [Open at least one colony to start a match.] Abra pelo menos uma colônia para iniciar uma partida. [Open book] @@ -1762,3 +1762,9 @@ Aplicar os gráficos ao carregar a próxima partida ou o editor de mapas (OpenGL Visão toroidal automática [settings Automatically show the torus overview while moving around the map (OpenGL).] Mostrar automaticamente a visão geral toroidal enquanto você se desloca pelo mapa (OpenGL). +[Channel width] +Largura do canal +[Land bridge width] +Largura da ponte de terra +[Lake size] +Tamanho do lago diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 34a9f531f..e9e6c20ec 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -611,9 +611,9 @@ dels quals [ok] OK [old islands terrain] -illes aleatòries antigues +Arxipèlag Abrupte [old random terrain] -vell terreny aleatori +Costa Fragmentada [on press] En prémer [On the spot] @@ -1371,9 +1371,9 @@ Mitjana - Experimental [No AI orders] Sense ordres d'IA [Old islands] -Illes (generador antic) +Arxipèlag Abrupte [Old random] -Aleatori (generador antic) +Costa Fragmentada [Open at least one colony to start a match.] Obre almenys una colònia per iniciar una partida. [Open book] @@ -1774,3 +1774,9 @@ Aplica els gràfics quan es carregui la propera partida o l’editor de mapes (O Vista toroidal automàtica [settings Automatically show the torus overview while moving around the map (OpenGL).] Mostra automàticament la vista general toroidal mentre us desplaceu pel mapa (OpenGL). +[Channel width] +Amplada del canal +[Land bridge width] +Amplada del pont de terra +[Lake size] +Mida del llac diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 026d20a00..c07e91b77 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -605,9 +605,9 @@ z toho [ok] Budiž [old islands terrain] -náhodné ostrovy (staré) +Členité souostroví [old random terrain] -náhodný terén (starý) +Roztříštěné pobřeží [on press] Stisk [On the spot] @@ -1363,9 +1363,9 @@ Střední – Experimentální [No AI orders] Žádné příkazy AI [Old islands] -Staré ostrovy +Členité souostroví [Old random] -Staré náhodné +Roztříštěné pobřeží [Open at least one colony to start a match.] Otevřete alespoň jednu kolonii a začněte zápas. [Open book] @@ -1766,3 +1766,9 @@ Vybraná grafika se použije při příštím načtení hry nebo editoru map (Op Automatický toroidní pohled [settings Automatically show the torus overview while moving around the map (OpenGL).] Automaticky zobrazovat přehled mapy ve tvaru torusu při pohybu po mapě (OpenGL). +[Channel width] +Šířka průlivu +[Land bridge width] +Šířka pevninského mostu +[Lake size] +Velikost jezera diff --git a/data/texts.de.txt b/data/texts.de.txt index 8b56bc6b6..d80760c9d 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -605,9 +605,9 @@ wovon [ok] Ok [old islands terrain] -alte Inselzufallskarte +Raues Archipel [old random terrain] -alte Zufallskarte +Zerklüftete Küste [on press] Drücken [On the spot] @@ -1363,9 +1363,9 @@ Mittel – Experimentell [No AI orders] Keine KI-Befehle [Old islands] -Inseln (alter Generator) +Raues Archipel [Old random] -Zufall (alter Generator) +Zerklüftete Küste [Open at least one colony to start a match.] Öffnen Sie mindestens eine Kolonie, um ein Match zu starten. [Open book] @@ -1766,3 +1766,9 @@ Grafik beim nächsten Laden einer Partie oder des Karteneditors anwenden (OpenGL Automatische Torusansicht [settings Automatically show the torus overview while moving around the map (OpenGL).] Zeigt automatisch die torusförmige Kartenübersicht an, während Sie sich auf der Karte bewegen (OpenGL). +[Channel width] +Kanalbreite +[Land bridge width] +Breite der Landbrücke +[Lake size] +Seegröße diff --git a/data/texts.dk.txt b/data/texts.dk.txt index a41a2ee83..791b7b3ab 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -677,9 +677,9 @@ heraf [ok] Ok [old islands terrain] -Gamle tilfældige øer +Barsk øgruppe [old random terrain] -Gammelt tilfældigt terræn +Opsplittet kyst [on press] Ved tryk [On the spot] @@ -1439,9 +1439,9 @@ Medium - Eksperimentel [No AI orders] Ingen AI-kommandoer [Old islands] -Gamle øer +Barsk øgruppe [Old random] -Gammel tilfældig +Opsplittet kyst [Open at least one colony to start a match.] Åbn mindst én koloni for at starte en kamp. [Open book] @@ -1842,3 +1842,9 @@ Anvend grafikken, næste gang et spil eller korteditoren indlæses (OpenGL). Automatisk torusvisning [settings Automatically show the torus overview while moving around the map (OpenGL).] Vis automatisk den torusformede kortoversigt, mens du bevæger dig rundt på kortet (OpenGL). +[Channel width] +Kanalbredde +[Land bridge width] +Landbroens bredde +[Lake size] +Søstørrelse diff --git a/data/texts.en.txt b/data/texts.en.txt index abc5c8e05..b3dfc3d06 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -619,9 +619,9 @@ there are [ok] Ok [old islands terrain] -old random islands +Rugged Archipelago [old random terrain] -old random terrain +Shattered Coast [on press] On Press [On the spot] @@ -1361,9 +1361,9 @@ Medium - Experimental [No AI orders] No AI orders [Old islands] -Old islands +Rugged Archipelago [Old random] -Old random +Shattered Coast [Open at least one colony to start a match.] Open at least one colony to start a match. [Open book] @@ -1764,3 +1764,9 @@ Apply artwork on the next game or editor load (OpenGL). Automatic torus view [settings Automatically show the torus overview while moving around the map (OpenGL).] Automatically show the torus overview while moving around the map (OpenGL). +[Channel width] +Channel width +[Land bridge width] +Land bridge width +[Lake size] +Lake size diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 2d8581aef..72f5c911d 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -607,9 +607,9 @@ da kiuj, estas [ok] Bone! [old islands terrain] -Insuloj, Hazarde +Kruta Insularo [old random terrain] -Tereno, Hazarde +Fragmentita Marbordo [on press] Premo [On the spot] @@ -1361,9 +1361,9 @@ Meza - Eksperimenta [No AI orders] Neniuj AI-komandoj [Old islands] -Insuloj (malnova generatoro) +Kruta Insularo [Old random] -Hazarda (malnova generatoro) +Fragmentita Marbordo [Open at least one colony to start a match.] Malfermu almenaŭ unu kolonion por komenci matĉon. [Open book] @@ -1764,3 +1764,9 @@ La bildoj estos aplikitaj ĉe la sekva ŝargo de ludo aŭ mapredaktilo (OpenGL). Aŭtomata tora vido [settings Automatically show the torus overview while moving around the map (OpenGL).] Aŭtomate montru la torforman superrigardon de la mapo dum moviĝado tra la mapo (OpenGL). +[Channel width] +Larĝo de kanalo +[Land bridge width] +Larĝo de terponto +[Lake size] +Grandeco de lago diff --git a/data/texts.es.txt b/data/texts.es.txt index 9b3b17b8f..f9ef275b7 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -607,9 +607,9 @@ de las cuales [ok] Aceptar [old islands terrain] -Antiguas islas aleatorias +Archipiélago Escarpado [old random terrain] -Antiguo terreno aleatorio +Costa Fragmentada [on press] Al pulsar [On the spot] @@ -1363,9 +1363,9 @@ Medio - Experimental [No AI orders] Sin comandos de IA [Old islands] -Islas (generador antiguo) +Archipiélago Escarpado [Old random] -Aleatorio (generador antiguo) +Costa Fragmentada [Open at least one colony to start a match.] Abre al menos una colonia para comenzar una partida. [Open book] @@ -1766,3 +1766,9 @@ Aplica los gráficos al cargar la próxima partida o el editor de mapas (OpenGL) Vista toroidal automática [settings Automatically show the torus overview while moving around the map (OpenGL).] Muestra automáticamente la vista general toroidal mientras te desplazas por el mapa (OpenGL). +[Channel width] +Anchura del canal +[Land bridge width] +Anchura del puente terrestre +[Lake size] +Tamaño del lago diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 0c2ed27ea..65685d74a 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -613,9 +613,9 @@ hauetatik [ok] Ados [old islands terrain] -ausazko irla lurzoru zaharra +Artxipelago Malkartsua [old random terrain] -ausazko lurzoru zaharra +Kostalde Zatikatua [on press] Sakatzean [On the spot] @@ -1373,9 +1373,9 @@ Ertaina - Esperimentala [No AI orders] Ez dago AI komandorik [Old islands] -Uharte zaharrak +Artxipelago Malkartsua [Old random] -Ausazko zaharra +Kostalde Zatikatua [Open at least one colony to start a match.] Ireki gutxienez kolonia bat partida bat hasteko. [Open book] @@ -1776,3 +1776,9 @@ Erabili hautatutako artelana joko bat edo mapa-editorea kargatzen den hurrengoan Toroide-ikuspegi automatikoa [settings Automatically show the torus overview while moving around the map (OpenGL).] Erakutsi automatikoki toroide formako maparen ikuspegi orokorra mapan mugitzen zaren bitartean (OpenGL). +[Channel width] +Kanalaren zabalera +[Land bridge width] +Lur-zubiaren zabalera +[Lake size] +Aintziraren tamaina diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 12b85bb48..c0021d92d 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -607,9 +607,9 @@ numlock [ok] قبول! [old islands terrain] -زمین تصادفی +مجمع‌الجزایر ناهموار [old random terrain] -مجمع‌الجزایر تصادفی +ساحل گسسته [on press] فشارداده [On the spot] @@ -1361,9 +1361,9 @@ Cortex [No AI orders] بدون دستور هوش مصنوعی [Old islands] -جزایر قدیمی +مجمع‌الجزایر ناهموار [Old random] -تصادفی قدیمی +ساحل گسسته [Open at least one colony to start a match.] حداقل یک کلنی را برای شروع مسابقه باز کنید. [Open book] @@ -1764,3 +1764,9 @@ OpenGL در این بیلد موجود نیست. نمای خودکار چنبره [settings Automatically show the torus overview while moving around the map (OpenGL).] هنگام حرکت در نقشه (OpenGL) نمای کلی نقشه به شکل چنبره را به صورت خودکار نشان دهید. +[Channel width] +پهنای آبراه +[Land bridge width] +پهنای پل خشکی +[Lake size] +اندازهٔ دریاچه diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 219a9752d..073828167 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -607,9 +607,9 @@ joista [ok] Ok [old islands terrain] -vanhat satunnaiset saaret +Rosoiset saaret [old random terrain] -vanhat satunnaiset maastot +Rikkonainen rannikko [on press] Painallus [On the spot] @@ -1363,9 +1363,9 @@ Keskitaso - Kokeellinen [No AI orders] Ei tekoälyn käskyjä [Old islands] -Vanhat saaret +Rosoiset saaret [Old random] -Vanha satunnainen +Rikkonainen rannikko [Open at least one colony to start a match.] Avaa vähintään yksi siirtokunta aloittaaksesi ottelun. [Open book] @@ -1766,3 +1766,9 @@ Ota grafiikka käyttöön, kun seuraava peli tai karttaeditori ladataan (OpenGL) Automaattinen torusnäkymä [settings Automatically show the torus overview while moving around the map (OpenGL).] Näytä toruksen muotoinen karttanäkymä automaattisesti liikkuessasi kartalla (OpenGL). +[Channel width] +Kanavan leveys +[Land bridge width] +Maasillan leveys +[Lake size] +Järven koko diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 5d0c5bba1..2c599e7e7 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -615,9 +615,9 @@ dont [ok] Accepter [old islands terrain] -Ancien terrain d'île +Archipel accidenté [old random terrain] -Ancien terrain aléatoire +Côte morcelée [on press] Appuyer [On the spot] @@ -1373,9 +1373,9 @@ Moyen - Expérimental [No AI orders] Aucun ordre de l’IA [Old islands] -Îles (ancien générateur) +Archipel accidenté [Old random] -Aléatoire (ancien générateur) +Côte morcelée [Open at least one colony to start a match.] Ouvrez au moins une colonie pour démarrer une partie. [Open book] @@ -1776,3 +1776,9 @@ Appliquer les graphismes au prochain chargement d’une partie ou de l’éditeu Vue torique automatique [settings Automatically show the torus overview while moving around the map (OpenGL).] Afficher automatiquement l'aperçu de la carte en forme de tore tout en vous déplaçant sur la carte (OpenGL). +[Channel width] +Largeur du chenal +[Land bridge width] +Largeur du pont terrestre +[Lake size] +Taille des lacs diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 63643dfc0..8fcca2740 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -673,9 +673,9 @@ numlock [ok] Εντάξει [old islands terrain] -παλιά τυχαία νησιά +Τραχύ Αρχιπέλαγος [old random terrain] -παλιά τυχαία πεδία +Κατακερματισμένη Ακτή [on press] Πάτημα [On the spot] @@ -1433,9 +1433,9 @@ Cortex [No AI orders] Χωρίς εντολές AI [Old islands] -Παλιά νησιά +Τραχύ Αρχιπέλαγος [Old random] -Παλιό τυχαίο +Κατακερματισμένη Ακτή [Open at least one colony to start a match.] Ανοίξτε τουλάχιστον μία αποικία για να ξεκινήσετε έναν αγώνα. [Open book] @@ -1836,3 +1836,9 @@ Cortex Αυτόματη τοροειδής προβολή [settings Automatically show the torus overview while moving around the map (OpenGL).] Αυτόματη εμφάνιση της επισκόπησης του χάρτη σε σχήμα τόρου ενώ μετακινείστε στον χάρτη (OpenGL). +[Channel width] +Πλάτος καναλιού +[Land bridge width] +Πλάτος χερσαίας γέφυρας +[Lake size] +Μέγεθος λίμνης diff --git a/data/texts.hu.txt b/data/texts.hu.txt index ba3f8442a..6b32c5615 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -605,9 +605,9 @@ ebből [ok] Rendben [old islands terrain] -régi véletlenszerű szigetek +Csipkézett szigetvilág [old random terrain] -régi véletlenszerű terep +Töredezett partvidék [on press] Lenyomás [On the spot] @@ -1363,9 +1363,9 @@ Közepes – Kísérleti [No AI orders] Az MI nem ad parancsokat [Old islands] -Régi szigetek +Csipkézett szigetvilág [Old random] -Régi véletlen +Töredezett partvidék [Open at least one colony to start a match.] Nyisson meg legalább egy kolóniát a mérkőzés elindításához. [Open book] @@ -1766,3 +1766,9 @@ A kiválasztott grafika a játék vagy a térképszerkesztő következő betölt Automatikus tórusznézet [settings Automatically show the torus overview while moving around the map (OpenGL).] A tórusz alakú térkép áttekintésének automatikus megjelenítése a térképen való mozgás közben (OpenGL). +[Channel width] +Csatorna szélessége +[Land bridge width] +Földhíd szélessége +[Lake size] +Tó mérete diff --git a/data/texts.id.txt b/data/texts.id.txt index 73bab062d..df16711f0 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -617,9 +617,9 @@ di antaranya [ok] Oke [old islands terrain] -pulau acak lama +Kepulauan Terjal [old random terrain] -medan acak lama +Pesisir Terpecah [on press] Ditekan [On the spot] @@ -1359,9 +1359,9 @@ Sedang - Eksperimental [No AI orders] Tidak ada perintah AI [Old islands] -Pulau versi lama +Kepulauan Terjal [Old random] -Acak lama +Pesisir Terpecah [Open at least one colony to start a match.] Buka setidaknya satu koloni untuk memulai pertandingan. [Open book] @@ -1762,3 +1762,9 @@ Grafis diterapkan saat permainan atau editor peta dimuat berikutnya (OpenGL). Tampilan torus otomatis [settings Automatically show the torus overview while moving around the map (OpenGL).] Secara otomatis menampilkan ikhtisar peta berbentuk torus saat bergerak di sekitar peta (OpenGL). +[Channel width] +Lebar kanal +[Land bridge width] +Lebar jembatan darat +[Lake size] +Ukuran danau diff --git a/data/texts.it.txt b/data/texts.it.txt index d1f94de42..686c87e0f 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -665,9 +665,9 @@ dei quali [ok] Ok [old islands terrain] -Vecchie isole casuali +Arcipelago Frastagliato [old random terrain] -Vecchio terreno casuale +Costa Frammentata [on press] Premuto [On the spot] @@ -1425,9 +1425,9 @@ Medio - Sperimentale [No AI orders] Nessun comando dell'IA [Old islands] -Isole (vecchio generatore) +Arcipelago Frastagliato [Old random] -Casuale (vecchio generatore) +Costa Frammentata [Open at least one colony to start a match.] Apri almeno una colonia per iniziare una partita. [Open book] @@ -1828,3 +1828,9 @@ Applica la grafica al prossimo caricamento di una partita o dell’editor di map Vista toroidale automatica [settings Automatically show the torus overview while moving around the map (OpenGL).] Mostra automaticamente la panoramica toroidale mentre ti sposti sulla mappa (OpenGL). +[Channel width] +Larghezza del canale +[Land bridge width] +Larghezza del ponte terrestre +[Lake size] +Dimensione del lago diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 59f1b0326..5658819b6 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -623,9 +623,9 @@ Num Lock [ok] OK [old islands terrain] -旧式のランダムな島々 +険しい群島 [old random terrain] -旧式のランダム地形 +入り組んだ海岸 [on press] 押した時 [On the spot] @@ -1359,9 +1359,9 @@ AIの戦略と対抗策 [No AI orders] AIの命令なし [Old islands] -旧式の島生成 +険しい群島 [Old random] -旧式のランダム生成 +入り組んだ海岸 [Open at least one colony to start a match.] 試合を始めるには、少なくとも1つのコロニーを参加させてください。 [Open book] @@ -1762,3 +1762,9 @@ Globulation 2 のカーソルを表示します。 自動トーラス表示 [settings Automatically show the torus overview while moving around the map (OpenGL).] マップ内を移動するときに、トーラス(ドーナツ)形状のマップ全体図を自動的に表示します (OpenGL)。 +[Channel width] +水路の幅 +[Land bridge width] +陸橋の幅 +[Lake size] +湖の大きさ diff --git a/data/texts.keys.txt b/data/texts.keys.txt index a1fe56a5c..ad88e7155 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -880,3 +880,6 @@ [settings Apply artwork on the next game or editor load (OpenGL).] [settings Automatic torus view] [settings Automatically show the torus overview while moving around the map (OpenGL).] +[Channel width] +[Land bridge width] +[Lake size] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index a30df9dbd..5c2796baf 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -617,9 +617,9 @@ Num Lock [ok] 확인 [old islands terrain] -기존 무작위 섬 +험준한 군도 [old random terrain] -기존 무작위 지형 +갈라진 해안 [on press] 누를 때 [On the spot] @@ -1359,9 +1359,9 @@ AI 전략 및 대응 [No AI orders] AI 명령 없음 [Old islands] -구형 섬 생성 +험준한 군도 [Old random] -구형 무작위 생성 +갈라진 해안 [Open at least one colony to start a match.] 매치를 시작하려면 콜로니를 하나 이상 여세요. [Open book] @@ -1762,3 +1762,9 @@ Globulation 2 커서를 표시합니다. 자동 토러스 보기 [settings Automatically show the torus overview while moving around the map (OpenGL).] 지도 안에서 이동할 때 토러스(도넛) 모양의 지도 전체 보기를 자동으로 표시합니다(OpenGL). +[Channel width] +수로 너비 +[Land bridge width] +육교 너비 +[Lake size] +호수 크기 diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 1f54e7dd1..730b051f3 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -627,9 +627,9 @@ waarvan [ok] Ok [old islands terrain] -Oude eilanden +Ruige Archipel [old random terrain] -Oud willekeurig terrein +Versplinterde Kust [on press] Indrukken [On the spot] @@ -1387,9 +1387,9 @@ Gemiddeld - Experimenteel [No AI orders] Geen AI-opdrachten [Old islands] -Eilanden (oude generator) +Ruige Archipel [Old random] -Willekeurig (oude generator) +Versplinterde Kust [Open at least one colony to start a match.] Open minstens één kolonie om een wedstrijd te starten. [Open book] @@ -1790,3 +1790,9 @@ Pas de grafische beelden toe wanneer een volgende partij of de kaarteditor wordt Automatische torusweergave [settings Automatically show the torus overview while moving around the map (OpenGL).] Toon automatisch het torusvormige kaartoverzicht terwijl u over de kaart beweegt (OpenGL). +[Channel width] +Kanaalbreedte +[Land bridge width] +Breedte van de landbrug +[Lake size] +Meergrootte diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 63df2cad3..e5867139a 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -607,9 +607,9 @@ z czego [ok] Ok [old islands terrain] -stare losowe wyspy +Surowy Archipelag [old random terrain] -stary losowy teren +Poszarpane Wybrzeże [on press] Wciśnięcie [On the spot] @@ -1363,9 +1363,9 @@ Mecz zakończony [No AI orders] Bez rozkazów SI [Old islands] -Stare wyspy +Surowy Archipelag [Old random] -Stary losowy +Poszarpane Wybrzeże [Open at least one colony to start a match.] Otwórz co najmniej jedną kolonię, aby rozpocząć mecz. [Open book] @@ -1766,3 +1766,9 @@ Wybrana grafika zostanie zastosowana przy następnym załadowaniu gry lub edytor Automatyczny widok torusa [settings Automatically show the torus overview while moving around the map (OpenGL).] Automatycznie pokazuj przegląd mapy w kształcie torusa podczas poruszania się po mapie (OpenGL). +[Channel width] +Szerokość kanału +[Land bridge width] +Szerokość mostu lądowego +[Lake size] +Wielkość jeziora diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 5a7e362a0..5fd8703fa 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -611,9 +611,9 @@ dos quais há [ok] Aceitar [old islands terrain] -ilhas aleatórias antigas +Arquipélago Acidentado [old random terrain] -terreno aleatório antigo +Costa Fragmentada [on press] Ao premir [On the spot] @@ -1369,9 +1369,9 @@ Médio - Experimental [No AI orders] Sem ordens da IA [Old islands] -Ilhas (gerador antigo) +Arquipélago Acidentado [Old random] -Aleatório (gerador antigo) +Costa Fragmentada [Open at least one colony to start a match.] Abra pelo menos uma colónia para iniciar uma partida. [Open book] @@ -1772,3 +1772,9 @@ Aplicar os gráficos ao carregar a próxima partida ou o editor de mapas (OpenGL Vista toroidal automática [settings Automatically show the torus overview while moving around the map (OpenGL).] Mostrar automaticamente a vista geral toroidal enquanto se desloca pelo mapa (OpenGL). +[Channel width] +Largura do canal +[Land bridge width] +Largura da ponte terrestre +[Lake size] +Tamanho do lago diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 3dc953a18..21f0c5f4e 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -605,9 +605,9 @@ dintre care [ok] Acceptă [old islands terrain] -insule aleatorii vechi +Arhipelag Accidentat [old random terrain] -teren aleatoriu vechi +Coastă Fragmentată [on press] La apăsare [On the spot] @@ -1363,9 +1363,9 @@ Mediu - Experimental [No AI orders] Fără ordine IA [Old islands] -Insule vechi +Arhipelag Accidentat [Old random] -Vechi aleatoriu +Coastă Fragmentată [Open at least one colony to start a match.] Deschide cel puțin o colonie pentru a începe un meci. [Open book] @@ -1766,3 +1766,9 @@ Grafica selectată se aplică la următoarea încărcare a jocului sau a editoru Vizualizare toroidală automată [settings Automatically show the torus overview while moving around the map (OpenGL).] Afișați automat prezentarea generală a hărții în formă de torus în timp ce vă deplasați pe hartă (OpenGL). +[Channel width] +Lățimea canalului +[Land bridge width] +Lățimea punții de uscat +[Lake size] +Mărimea lacului diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 88553314a..19cbac446 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -607,9 +607,9 @@ numlock [ok] ок [old islands terrain] -старые случайные острова +Скалистый архипелаг [old random terrain] -старый случайный ландшафт +Изрезанный берег [on press] Нажата [On the spot] @@ -1361,9 +1361,9 @@ Cortex [No AI orders] Никаких команд ИИ [Old islands] -Старые острова +Скалистый архипелаг [Old random] -Старый рандом +Изрезанный берег [Open at least one colony to start a match.] Откройте хотя бы одну колонию, чтобы начать матч. [Open book] @@ -1764,3 +1764,9 @@ OpenGL недоступен в этой сборке. Автоматический вид тора [settings Automatically show the torus overview while moving around the map (OpenGL).] Автоматически отображать обзор карты в форме тора при перемещении по карте (OpenGL). +[Channel width] +Ширина пролива +[Land bridge width] +Ширина перешейка +[Lake size] +Размер озера diff --git a/data/texts.si.txt b/data/texts.si.txt index b1706a81f..baa55b441 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -605,9 +605,9 @@ od katerih [ok] Potrdi [old islands terrain] -naključni otoki (staro) +Razgibano otočje [old random terrain] -naključni teren (staro) +Razdrobljena obala [on press] Pritisk [On the spot] @@ -1363,9 +1363,9 @@ Srednje – eksperimentalno [No AI orders] Brez ukazov UI [Old islands] -Stari otoki +Razgibano otočje [Old random] -Staro naključje +Razdrobljena obala [Open at least one colony to start a match.] Odprite vsaj eno kolonijo, da začnete tekmo. [Open book] @@ -1766,3 +1766,9 @@ Izbrana grafika se uporabi ob naslednjem nalaganju igre ali urejevalnika zemljev Samodejni torusni pogled [settings Automatically show the torus overview while moving around the map (OpenGL).] Samodejno prikaži pregled zemljevida v obliki torusa med premikanjem po zemljevidu (OpenGL). +[Channel width] +Širina kanala +[Land bridge width] +Širina kopenskega mostu +[Lake size] +Velikost jezera diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 8ce179f40..22a960688 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -611,9 +611,9 @@ z nich [ok] Ok [old islands terrain] -staré náhodné ostrovy +Členité súostrovie [old random terrain] -starý náhodný terén +Roztrieštené pobrežie [on press] Stlačenie [On the spot] @@ -1371,9 +1371,9 @@ Stredná - Experimentálna [No AI orders] Žiadne príkazy AI [Old islands] -Staré ostrovy +Členité súostrovie [Old random] -Staré náhodné +Roztrieštené pobrežie [Open at least one colony to start a match.] Otvorte aspoň jednu kolóniu a začnite zápas. [Open book] @@ -1774,3 +1774,9 @@ Vybraná grafika sa použije pri ďalšom načítaní hry alebo editora máp (Op Automatické toroidné zobrazenie [settings Automatically show the torus overview while moving around the map (OpenGL).] Automaticky zobraziť prehľad mapy v tvare torusu pri pohybe po mape (OpenGL). +[Channel width] +Šírka prielivu +[Land bridge width] +Šírka pevninského mosta +[Lake size] +Veľkosť jazera diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 9356daf8e..23106af64 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -605,9 +605,9 @@ Num Lock [ok] У реду [old islands terrain] -насумична острва (старо) +Кршевити архипелаг [old random terrain] -насумични терен (старо) +Разуђена обала [on press] Притисак [On the spot] @@ -1363,9 +1363,9 @@ Cortex [No AI orders] Без наређења ВИ [Old islands] -Стара острва +Кршевити архипелаг [Old random] -Стари случајни +Разуђена обала [Open at least one colony to start a match.] Отворите бар једну колонију да бисте започели меч. [Open book] @@ -1766,3 +1766,9 @@ OpenGL није доступан у овој верзији. Аутоматски торусни приказ [settings Automatically show the torus overview while moving around the map (OpenGL).] Аутоматски прикажи преглед мапе у облику торуса током кретања по мапи (OpenGL). +[Channel width] +Ширина канала +[Land bridge width] +Ширина копненог моста +[Lake size] +Величина језера diff --git a/data/texts.sv.txt b/data/texts.sv.txt index a8152a4f2..8a15e9678 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -613,9 +613,9 @@ varav [ok] Ok [old islands terrain] -Gamla slumpmässiga öar +Karg skärgård [old random terrain] -Gammal slumpmässig terräng +Splittrad kust [on press] Vid tryck [On the spot] @@ -1373,9 +1373,9 @@ Medium - Experimentell [No AI orders] Inga AI-kommandon [Old islands] -Gamla öar +Karg skärgård [Old random] -Äldre slumpgenerator +Splittrad kust [Open at least one colony to start a match.] Öppna minst en koloni för att starta en match. [Open book] @@ -1776,3 +1776,9 @@ Använd grafiken nästa gång ett spel eller kartredigeraren laddas (OpenGL). Automatisk torusvy [settings Automatically show the torus overview while moving around the map (OpenGL).] Visa automatiskt den torusformade kartöversikten medan du flyttar runt på kartan (OpenGL). +[Channel width] +Kanalbredd +[Land bridge width] +Landbrons bredd +[Lake size] +Sjöstorlek diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 53860b8ff..c35b16ce6 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -611,9 +611,9 @@ ayrintilar: [ok] Tamam [old islands terrain] -eski rastgele adalar +Engebeli Takımadalar [old random terrain] -eski rastgele arazi +Parçalı Kıyı [on press] Basınca [On the spot] @@ -1371,9 +1371,9 @@ Orta - Deneysel [No AI orders] Yapay zekâ emri yok [Old islands] -Eski adalar +Engebeli Takımadalar [Old random] -Eski rastgele +Parçalı Kıyı [Open at least one colony to start a match.] Bir maça başlamak için en az bir koloni açın. [Open book] @@ -1774,3 +1774,9 @@ Seçilen grafikler, oyun veya harita düzenleyici bir sonraki yüklendiğinde uy Otomatik torus görünümü [settings Automatically show the torus overview while moving around the map (OpenGL).] Haritada dolaşırken torus şeklindeki haritaya genel bakışı otomatik olarak gösterin (OpenGL). +[Channel width] +Kanal genişliği +[Land bridge width] +Kara köprüsü genişliği +[Lake size] +Göl boyutu diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 7599531a6..122a0c6c6 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -623,9 +623,9 @@ Num Lock [ok] Гаразд [old islands terrain] -старі випадкові острови +Скелястий архіпелаг [old random terrain] -старий випадковий ландшафт +Порізане узбережжя [on press] Натиск [On the spot] @@ -1359,9 +1359,9 @@ Ysagoon Online Gaming [No AI orders] Без команд ШІ [Old islands] -Старі острови +Скелястий архіпелаг [Old random] -Старий випадковий +Порізане узбережжя [Open at least one colony to start a match.] Відкрийте принаймні одну колонію, щоб почати матч. [Open book] @@ -1762,3 +1762,9 @@ OpenGL недоступний у цій збірці. Автоматичний вигляд тора [settings Automatically show the torus overview while moving around the map (OpenGL).] Автоматично показувати огляд карти у формі тора під час переміщення по карті (OpenGL). +[Channel width] +Ширина протоки +[Land bridge width] +Ширина перешийка +[Lake size] +Розмір озера diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 7051e8f5c..a8b257ddc 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -617,9 +617,9 @@ trong đó có [ok] Đồng ý [old islands terrain] -đảo ngẫu nhiên kiểu cũ +Quần Đảo Gồ Ghề [old random terrain] -địa hình ngẫu nhiên kiểu cũ +Bờ Biển Chia Cắt [on press] Khi nhấn [On the spot] @@ -1359,9 +1359,9 @@ Trung bình - Thử nghiệm [No AI orders] Không có lệnh AI [Old islands] -Đảo kiểu cũ +Quần Đảo Gồ Ghề [Old random] -Ngẫu nhiên kiểu cũ +Bờ Biển Chia Cắt [Open at least one colony to start a match.] Mở ít nhất một thuộc địa để bắt đầu trận đấu. [Open book] @@ -1762,3 +1762,9 @@ Hình ảnh được áp dụng khi tải ván chơi hoặc trình chỉnh sửa Chế độ xem hình xuyến tự động [settings Automatically show the torus overview while moving around the map (OpenGL).] Tự động hiển thị tổng quan bản đồ hình xuyến khi di chuyển xung quanh bản đồ (OpenGL). +[Channel width] +Độ rộng kênh +[Land bridge width] +Độ rộng cầu đất +[Lake size] +Kích thước hồ diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 728759744..fbbb43596 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -605,9 +605,9 @@ numlock [ok] 确定 [old islands terrain] -古代岛屿地形 +崎岖群岛 [old random terrain] -古代随机地形 +破碎海岸 [on press] 按下时 [On the spot] @@ -1359,9 +1359,9 @@ AI策略与对抗 [No AI orders] 没有AI命令 [Old islands] -旧版岛屿生成 +崎岖群岛 [Old random] -旧版随机生成 +破碎海岸 [Open at least one colony to start a match.] 至少启用一个殖民地才能开始对局。 [Open book] @@ -1762,3 +1762,9 @@ OpenGL 在此版本中不可用。 自动环面视图 [settings Automatically show the torus overview while moving around the map (OpenGL).] 在地图上移动时自动显示圆环形状的地图概览 (OpenGL)。 +[Channel width] +水道宽度 +[Land bridge width] +陆桥宽度 +[Lake size] +湖泊大小 diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index b47c4de3b..414321201 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -673,9 +673,9 @@ Num Lock 鍵 [ok] 確定 [old islands terrain] -隨機島嶼(舊版) +崎嶇群島 [old random terrain] -隨機地形(舊版) +破碎海岸 [on press] 按下時 [On the spot] @@ -1435,9 +1435,9 @@ AI策略與對抗 [No AI orders] 沒有AI命令 [Old islands] -舊版島嶼產生器 +崎嶇群島 [Old random] -舊版隨機產生器 +破碎海岸 [Open at least one colony to start a match.] 至少啟用一個殖民地才能開始對局。 [Open book] @@ -1838,3 +1838,9 @@ OpenGL 在此版本中不可用。 自動環面視圖 [settings Automatically show the torus overview while moving around the map (OpenGL).] 在地圖上移動時自動顯示圓環形狀的地圖概覽 (OpenGL)。 +[Channel width] +水道寬度 +[Land bridge width] +陸橋寬度 +[Lake size] +湖泊大小 diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md new file mode 100644 index 000000000..efc39c023 --- /dev/null +++ b/docs/map-generators/README.md @@ -0,0 +1,13 @@ +# 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. + +## 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. diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 99e839b17..7ef14b0a3 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -22,9 +22,14 @@ namespace { std::string tr(const std::string &s) { return Toolkit::getStringTable()->getString("[" + s + "]"); } const Uint32 A = ALIGN_SCREEN_CENTERED; -const std::vector methods = { - "Swamp", "River", "Islands", "Crater lakes", "Concrete islands", - "Isles", "Old random", "Old islands"}; +const std::vector methods = [] +{ + std::vector result; + for (int m = MapGenerationDescriptor::eSWAMP; m <= MapGenerationDescriptor::eOLDISLANDS; ++m) + result.emplace_back( + MapGenerationDescriptor::methodName(static_cast(m))); + return result; +}(); std::vector localized(std::vector v) { for (auto &s : v) @@ -793,15 +798,19 @@ 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, 1, - 8, - [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, + 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(); + }); 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 " @@ -877,45 +886,43 @@ 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.generator.method = (MapGenerationDescriptor::Method)(value + 1); - changed(); - }); + 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(); + }); yy += 40; - auto discrete = [&](const std::string &id, const char *label, int current, int lo, int hi, - bool power, std::function apply) + auto discrete = [&](const MapGenerationDescriptor::Control &c, const std::string &id) { - ui.text(x + 4, yy + 7, tr(label), "little", 110, true); + ui.text(x + 4, yy + 7, tr(c.label), "little", 110, true); std::vector options; - for (int i = lo; i <= hi; ++i) - options.push_back(std::to_string(power ? (1 << i) : i)); - ui.dropdown(id, {x + 112, yy, leftW - 128, 28}, options, current - lo, - [apply, lo](int i) { apply(i + lo); }); + 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, + [this, c, changed](int i) + { + c.set(setup.generator, c.minimum + i * c.step); + if (c.field == &MapGenerationDescriptor::nbTeams) + setup.setCapacity(setup.generator.nbTeams); + changed(); + }); yy += 36; }; - discrete("generator/width", "Width", g.wDec, 6, 9, true, - [this, changed](int v) - { - setup.generator.wDec = v; - changed(); - }); - discrete("generator/height", "Height", g.hDec, 6, 9, true, - [this, changed](int v) - { - setup.generator.hDec = v; - changed(); - }); - discrete("generator/colonies", "Colonies", setup.capacity, 2, 12, false, - [this](int v) - { - setup.setCapacity(v); - setup.generator.nbTeams = v; - ++setup.mapRevision; - invalidate(); - }); + for (const auto &c : MapGenerationDescriptor::sharedControls()) + if (c.field != &MapGenerationDescriptor::nbWorkers) + { + if (c.field == &MapGenerationDescriptor::nbTeams) + g.nbTeams = setup.capacity; + const char *id = c.field == &MapGenerationDescriptor::wDec ? "generator/width" + : c.field == &MapGenerationDescriptor::hDec ? "generator/height" + : "generator/colonies"; + discrete(c, id); + } + ui.text(x + 4, yy, tr("Starting workers: Game Rules tab."), "little", leftW - 20, true); yy += 27; for (int section = 0; section < 3; ++section) @@ -928,93 +935,44 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) yy += 38; if (!expanded[section]) continue; - bool modern = g.method >= 1 && g.method <= 4, old = g.method == 7; - auto numeric = [&](const char *label, int *value, int lo, int hi) - { - std::string id = "generator/" + std::string(label); - ui.text(x + 5, yy, tr(label), "little", leftW - 55, true); - ui.text(x + leftW - 45, yy, std::to_string(*value), "little", 35); - yy += 18; - if (hi - lo > 8) - ui.slider(id, {x + 8, yy, leftW - 32, 22}, *value, lo, hi, - [value, changed](int v) - { - if (*value != v) - { - *value = v; - changed(); - } - }); - else - ui.stepper(id, {x + 8, yy, 130, 26}, *value, lo, hi, - [value, changed](int v) - { - *value = v; - changed(); - }); - yy += 34; - }; - if (section == 0) + bool any = false; + for (const auto &c : MapGenerationDescriptor::controls(g.method)) { - if (modern || old) + if (static_cast(c.group) != section) + continue; + any = true; + std::string id = "generator/" + std::string(c.label); + if (c.powerOfTwo) { - numeric("Water weight", &g.waterRatio, 0, 64); - if (g.method != 1) - numeric("Sand weight", &g.sandRatio, 0, 64); - numeric("Grass weight", &g.grassRatio, 0, 64); - if (modern && g.method != 1) - numeric("Desert weight", &g.desertRatio, 0, 64); - numeric("Smoothing", &g.smooth, 1, 8); + discrete(c, "generator/repeat"); + continue; } - if (g.method == 2) - numeric("River width", &g.riverDiameter, 1, 64); - if (g.method == 3) - numeric("Extra islands", &g.extraIslands, 0, 8); - if (g.method == 4) - numeric("Lake density", &g.craterDensity, 1, 64); - if (g.method == 8) - { - numeric("Island size", &g.oldIslandSize, 1, 64); - numeric("Beach size", &g.oldBeach, 0, 4); - } - if (g.method == 5 || g.method == 6) - { - yy += ui.paragraph(x + 5, yy, leftW - 22, - tr("This landscape uses a fixed terrain recipe.")) + - 8; - } - } - else if (section == 1) - { - if (modern || old) + ui.text(x + 5, yy, tr(c.label), "little", leftW - 55, true); + ui.text(x + leftW - 45, yy, std::to_string(c.get(g)), "little", 35); + yy += 18; + auto apply = [this, c, changed](int v) { - numeric("Wheat", &g.wheatRatio, 0, 64); - numeric("Wood", &g.woodRatio, 0, 64); - numeric("Stone", &g.stoneRatio, 0, 64); - numeric("Algae", &g.algaeRatio, 0, 64); - if (modern) - numeric("Fruit", &g.fruitRatio, 0, 64); - } - else - yy += ui.paragraph(x + 5, yy, leftW - 22, - tr("This landscape uses fixed resource placement.")) + - 8; - } - else - { - if (modern) - discrete("generator/repeat", "Repeat landscape", g.logRepeatAreaTimes, 0, 5, - true, - [this, changed](int v) - { - setup.generator.logRepeatAreaTimes = v; - changed(); - }); + if (c.get(setup.generator) != c.normalize(v)) + { + c.set(setup.generator, v); + changed(); + } + }; + if ((c.maximum - c.minimum) / c.step > 8) + ui.slider(id, {x + 8, yy, leftW - 32, 22}, (c.get(g) - c.minimum) / c.step, 0, + (c.maximum - c.minimum) / c.step, + [c, apply](int i) { apply(c.minimum + i * c.step); }); else - yy += ui.paragraph(x + 5, yy, leftW - 22, - tr("Dimensions and colony count are set above.")) + - 8; + ui.stepper(id, {x + 8, yy, 130, 26}, c.get(g), c.minimum, c.maximum, apply, + c.step); + yy += 34; } + if (!any) + yy += + ui.paragraph(x + 5, yy, leftW - 22, + tr(section == 1 ? "This landscape uses fixed resource placement." + : "Dimensions and colony count are set above.")) + + 8; } ui.endRegion(yy - startY + 8); } diff --git a/src/CustomGameSetup.h b/src/CustomGameSetup.h index aa2cf18ea..1c13c4109 100644 --- a/src/CustomGameSetup.h +++ b/src/CustomGameSetup.h @@ -40,7 +40,9 @@ struct CustomGameSetup case 3: return speed != 0; case 4: - return random && generator.nbWorkers != 4; + return random && generator.nbWorkers != MapGenerationDescriptor::control( + generator.method, "Starting workers") + .defaultValue; default: return false; } @@ -53,7 +55,8 @@ struct CustomGameSetup }; std::array colonies; MapGenerationDescriptor generator; - int capacity = 4; + MapGenerationHistory generatorHistory; + int capacity; bool random = false, prestige = true, revealed = false, locked = true; int speed = 0; std::string format = "FFA", ruleset = "Standard"; @@ -61,7 +64,8 @@ struct CustomGameSetup unsigned mapRevision = 0; CustomGameSetup() { - generator.method = MapGenerationDescriptor::eRIVER; + generator.setMethodDefaults(MapGenerationDescriptor::eRIVER); + capacity = generator.nbTeams; for (int i = 0; i < Team::MAX_COUNT; ++i) colonies[i].alliance = i; colonies[0].controller = Human; @@ -139,7 +143,9 @@ struct CustomGameSetup revealed = preset == 2; locked = true; speed = preset == 1 ? 3 : 0; - int workers = preset == 1 ? 8 : 4; + const auto &workerControl = + MapGenerationDescriptor::control(generator.method, "Starting workers"); + int workers = preset == 1 ? workerControl.maximum : workerControl.defaultValue; if (generator.nbWorkers != workers) { generator.nbWorkers = workers; @@ -154,18 +160,7 @@ struct CustomGameSetup { if (capacity < 1 || capacity > Team::MAX_COUNT) return "Invalid colony count."; - if (random && generator.method >= MapGenerationDescriptor::eSWAMP && - generator.method <= MapGenerationDescriptor::eOLDRANDOM && - generator.method != MapGenerationDescriptor::eCONCRETEISLANDS && - generator.method != MapGenerationDescriptor::eISLES && - generator.waterRatio + generator.grassRatio + - (generator.method == MapGenerationDescriptor::eSWAMP - ? 0 - : generator.sandRatio + - (generator.method == MapGenerationDescriptor::eOLDRANDOM - ? 0 - : generator.desertRatio)) == - 0) + if (random && !generator.hasTerrainWeight()) return "Give at least one terrain type a nonzero weight."; if (controllerCount() > Team::MAX_COUNT) return "Shared control needs a free controller slot (maximum 12)."; diff --git a/src/Glob2.cpp b/src/Glob2.cpp index 0e5bded0f..de9f0bc2a 100644 --- a/src/Glob2.cpp +++ b/src/Glob2.cpp @@ -168,40 +168,19 @@ int Glob2::runTestMapGeneration() { MapGenerationDescriptor descriptor; - int type = (syncRand() % 7) + 1; - int wDec = (syncRand() % 4) + 6; - int hDec = (syncRand() % 4) + 6; - int teams = (syncRand() % 12) + 1; - int workers = (syncRand() % 8) + 1; - int repeat = (syncRand() % 5); - int smooth = (syncRand() % 8) + 1; - - int oldBeach = (syncRand() % 4); - - descriptor.method = static_cast(type); - descriptor.nbTeams = teams; - descriptor.wDec=wDec; - descriptor.hDec=hDec; - descriptor.smooth = smooth; - descriptor.oldBeach=oldBeach; - descriptor.nbWorkers=workers; - descriptor.logRepeatAreaTimes = repeat; - - descriptor.waterRatio=syncRand() % 100; - descriptor.sandRatio=syncRand() % 100; - descriptor.grassRatio=syncRand() % 100; - descriptor.desertRatio=syncRand() % 100; - descriptor.wheatRatio=syncRand() % 100; - descriptor.woodRatio=syncRand() % 100; - descriptor.algaeRatio=syncRand() % 100; - descriptor.stoneRatio=syncRand() % 100; - descriptor.fruitRatio=syncRand() % 100; - descriptor.riverDiameter=syncRand() % 100; - descriptor.craterDensity=syncRand() % 100; - descriptor.extraIslands=syncRand() % 9; - //eISLANDS - descriptor.oldIslandSize=syncRand() % 74; - + using D = MapGenerationDescriptor; + auto method = static_cast(D::eSWAMP + syncRand() % (D::METHOD_COUNT - 1)); + descriptor.setMethodDefaults(method); + auto controls = D::sharedControls(); + const auto& specific = D::controls(method); + controls.insert(controls.end(), specific.begin(), specific.end()); + for (const auto& control : controls) + { + int choices = (control.maximum - control.minimum) / control.step + 1; + control.set(descriptor, control.minimum + (syncRand() % choices) * control.step); + } + if (!descriptor.hasTerrainWeight()) + continue; std::cout<<"Generating Map"< apply) + std::function apply, int step = 1) { - button(id + "/-", {r.x, r.y, 30, r.h}, "-", [=] { apply(value - 1); }, false, value > lo); + button( + id + "/-", {r.x, r.y, 30, r.h}, "-", [=] { apply(std::max(lo, value - step)); }, false, + value > lo); text(r.x + 42, r.y + 5, std::to_string(value)); button( - id + "/+", {r.x + r.w - 30, r.y, 30, r.h}, "+", [=] { apply(value + 1); }, false, - value < hi); + id + "/+", {r.x + r.w - 30, r.y, 30, r.h}, "+", + [=] { apply(std::min(hi, value + step)); }, false, value < hi); } void slider(const std::string &id, SDL_Rect r, int value, int lo, int hi, std::function apply) diff --git a/src/NewMapScreen.cpp b/src/NewMapScreen.cpp index 73db1621c..0735943be 100644 --- a/src/NewMapScreen.cpp +++ b/src/NewMapScreen.cpp @@ -2,468 +2,127 @@ // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière #include "NewMapScreen.h" -#include +#include "GlobalContainer.h" #include #include -using namespace GAGCore; #include #include -#include #include #include +#include +using namespace GAGCore; using namespace GAGGUI; +namespace +{ +std::string tr(const char *label) +{ + return Toolkit::getStringTable()->getString(std::string("[") + label + "]"); +} +constexpr Uint32 A = ALIGN_SCREEN_CENTERED; +} // namespace NewMapScreen::NewMapScreen() { - mapSizeX=new Number(20, 50, 100, 20, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 20, "menu"); - mapSizeX->add(64); - mapSizeX->add(128); - mapSizeX->add(256); - mapSizeX->add(512); - mapSizeX->setNth(descriptor.wDec-6); - addWidget(mapSizeX); - - mapSizeY=new Number(20, 75, 100, 20, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 20, "menu"); - mapSizeY->add(64); - mapSizeY->add(128); - mapSizeY->add(256); - mapSizeY->add(512); - mapSizeY->setNth(descriptor.hDec-6); - addWidget(mapSizeY); - - logRepeatAreaTimes=new Number(310, 75, 114, 20, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 20, "menu"); - logRepeatAreaTimes->add(1); - logRepeatAreaTimes->add(2); - logRepeatAreaTimes->add(4); - logRepeatAreaTimes->add(8); - logRepeatAreaTimes->add(16); - logRepeatAreaTimes->add(32); - logRepeatAreaTimes->visible=false; - addWidget(logRepeatAreaTimes); - - methods=new List(20, 100, 280, 300, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "menu"); - methods->addText(Toolkit::getStringTable()->getString("[uniform terrain]")); - methods->addText(Toolkit::getStringTable()->getString("[swamp terrain]")); - methods->addText(Toolkit::getStringTable()->getString("[river terrain]")); - methods->addText(Toolkit::getStringTable()->getString("[islands terrain]")); - methods->addText(Toolkit::getStringTable()->getString("[crater lakes terrain]")); - methods->addText(Toolkit::getStringTable()->getString("[concrete islands terrain]")); - methods->addText(Toolkit::getStringTable()->getString("[isles terrain]")); - methods->addText(Toolkit::getStringTable()->getString("[old random terrain]")); - methods->addText(Toolkit::getStringTable()->getString("[old islands terrain]")); - methods->setSelectionIndex(0); + 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); addWidget(methods); - - // eUNIFORM - - terrains=new List(340, 100, 280, 300, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "menu"); - terrains->addText(Toolkit::getStringTable()->getString("[water]")); - terrains->addText(Toolkit::getStringTable()->getString("[sand]")); - terrains->addText(Toolkit::getStringTable()->getString("[grass]")); + terrains = new List(340, 100, 280, 300, A, A, "menu"); + for (const char *name : {"water", "sand", "grass"}) + terrains->addText(tr(name)); terrains->setSelectionIndex(descriptor.terrainType); addWidget(terrains); - - // not eUNIFORM"", -1, -1, - - nbTeams=new Number(310, 100, 114, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 18, "menu"); - nbTeams->add(1); - nbTeams->add(2); - nbTeams->add(3); - nbTeams->add(4); - nbTeams->add(5); - nbTeams->add(6); - nbTeams->add(7); - nbTeams->add(8); - nbTeams->add(9); - nbTeams->add(10); - nbTeams->add(11); - nbTeams->add(12); - nbTeams->setNth(descriptor.nbTeams-1); - nbTeams->visible=false; - addWidget(nbTeams); - - nbWorkers=new Number(310, 120, 114, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 18, "menu"); - nbWorkers->add(1); - nbWorkers->add(2); - nbWorkers->add(3); - nbWorkers->add(4); - nbWorkers->add(5); - nbWorkers->add(6); - nbWorkers->add(7); - nbWorkers->add(8); - nbWorkers->setNth(descriptor.nbWorkers-1); - nbWorkers->visible=false; - addWidget(nbWorkers); - - numberOfTeamText=new Text(430, 100, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[number of teams]")); - numberOfTeamText->visible=false; - addWidget(numberOfTeamText); - - areaTimesText=new Text(430, 75, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[repeat area]")); - areaTimesText->visible=false; - addWidget(areaTimesText); - numberOfWorkerText=new Text (430, 120, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[workers]")); - numberOfWorkerText->visible=false; - addWidget(numberOfWorkerText); - - waterRatio=new Ratio(310, 160, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.waterRatio, "menu"); - waterRatio->set(50); - waterRatio->visible=false; - addWidget(waterRatio); - - sandRatio=new Ratio(310, 180, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.sandRatio, "menu"); - sandRatio->set(0); - sandRatio->visible=false; - addWidget(sandRatio); - - grassRatio=new Ratio(310, 200, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.grassRatio, "menu"); - grassRatio->set(50); - grassRatio->visible=false; - addWidget(grassRatio); - - desertRatio=new Ratio(310, 220, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.desertRatio, "menu"); - desertRatio->set(0); - desertRatio->visible=false; - addWidget(desertRatio); - - algaeRatio=new Ratio(310, 240, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.algaeRatio, "menu"); - algaeRatio->set(50); - algaeRatio->visible=false; - addWidget(algaeRatio); - - wheatRatio=new Ratio(310, 260, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.wheatRatio, "menu"); - wheatRatio->set(50); - wheatRatio->visible=false; - addWidget(wheatRatio); - - woodRatio=new Ratio(310, 280, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.woodRatio, "menu"); - woodRatio->set(50); - woodRatio->visible=false; - addWidget(woodRatio); - - stoneRatio=new Ratio(310, 300, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.stoneRatio, "menu"); - stoneRatio->set(50); - stoneRatio->visible=false; - addWidget(stoneRatio); - - fruitRatio=new Ratio(310, 320, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.fruitRatio, "menu"); - fruitRatio->set(4); - fruitRatio->visible=false; - addWidget(fruitRatio); - - riverDiameter=new Ratio(310, 340, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.riverDiameter, "menu"); - riverDiameter->set(50); - riverDiameter->visible=false; - addWidget(riverDiameter); - - craterDensity=new Ratio(310, 340, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 64, descriptor.craterDensity, "menu"); - craterDensity->set(50); - craterDensity->visible=false; - addWidget(craterDensity); - - extraIslands=new Number(310, 340, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 18, "menu"); - extraIslands->add(0); - extraIslands->add(1); - extraIslands->add(2); - extraIslands->add(3); - extraIslands->add(4); - extraIslands->add(5); - extraIslands->add(6); - extraIslands->add(7); - extraIslands->add(8); - extraIslands->setNth(descriptor.extraIslands); - extraIslands->visible=false; - addWidget(extraIslands); - - smooth=new Number(310, 360, 164, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 18, "menu"); - smooth->add(1); - smooth->add(2); - smooth->add(3); - smooth->add(4); - smooth->add(5); - smooth->add(6); - smooth->add(7); - smooth->add(8); - smooth->setNth(descriptor.smooth-1); - smooth->visible=false; - addWidget(smooth); - - ratioText=new Text(310, 140, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[ratios]")); - ratioText->visible=false; - addWidget(ratioText); - waterText=new Text(480, 160, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[water]")); - waterText->visible=false; - addWidget(waterText); - sandText=new Text(480, 180, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[sand]")); - sandText->visible=false; - addWidget(sandText); - grassText=new Text(480, 200, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[grass]")); - grassText->visible=false; - addWidget(grassText); - desertText=new Text(480, 220, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[desert]")); - desertText->visible=false; - addWidget(desertText); - algaeText=new Text(480, 240, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[algae]")); - algaeText->visible=false; - addWidget(algaeText); - wheatText=new Text(480, 260, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[Wheat]")); - wheatText->visible=false; - addWidget(wheatText); - woodText=new Text(480, 280, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[wood]")); - woodText->visible=false; - addWidget(woodText); - stoneText=new Text(480, 300, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[stone]")); - stoneText->visible=false; - addWidget(stoneText); - fruitText=new Text(480, 320, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[fruit]")); - fruitText->visible=false; - addWidget(fruitText); - riverDiameterText=new Text(480, 340, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[river diameter]")); - riverDiameterText->visible=false; - addWidget(riverDiameterText); - craterDensityText=new Text(480, 340, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[crater density]")); - craterDensityText->visible=false; - addWidget(craterDensityText); - extraIslandsText=new Text(480, 340, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[extra islands]")); - extraIslandsText->visible=false; - addWidget(extraIslandsText); - smoothingText=new Text(480, 360, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[smoothing]")); - smoothingText->visible=false; - addWidget(smoothingText); - // eOLDISLANDS - - oldIslandSize=new Ratio(310, 140, 114, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 40, descriptor.oldIslandSize, "menu"); - oldIslandSize->visible=false; - addWidget(oldIslandSize); - - oldBeach=new Number(310, 160, 114, 18, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, 18, "menu"); - oldBeach->add(0); - oldBeach->add(1); - oldBeach->add(2); - oldBeach->add(3); - oldBeach->add(4); - oldBeach->setNth(descriptor.oldBeach); - oldBeach->visible=false; - addWidget(oldBeach); - - oldIslandSizeText=new Text(430, 140, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[islands size]")); - oldIslandSizeText->visible=false; - addWidget(oldIslandSizeText); - oldBeachSizeText=new Text(430, 160, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[beach size]")); - oldBeachSizeText->visible=false; - addWidget(oldBeachSizeText); - - // all - - addWidget(new TextButton(10, 420, 300, 40, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[ok]"), OK, 13)); - addWidget(new TextButton(330, 420, 300, 40, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[Cancel]"), CANCEL, 27)); + auto addControl = [&](const MapGenerationDescriptor::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); + 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()) + { + 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; + addControl(c, -1, size ? 20 : 310, y); + } + for (int m = 0; m <= MapGenerationDescriptor::eOLDISLANDS; ++m) + { + int y = 160; + for (const auto &c : + MapGenerationDescriptor::controls(static_cast(m))) + { + bool repeat = c.group == MapGenerationDescriptor::ControlGroup::Layout; + addControl(c, m, 310, repeat ? 75 : y); + if (!repeat) + y += 20; + } + } + updateControls(); + addWidget(new TextButton(10, 420, 300, 40, A, A, "menu", tr("ok"), OK, 13)); + addWidget(new TextButton(330, 420, 300, 40, A, A, "menu", tr("Cancel"), CANCEL, 27)); + addWidget(new Text(0, 18, ALIGN_FILL, A, "menu", tr("create map"))); +} - const std::string text= Toolkit::getStringTable()->getString("[create map]"); - addWidget(new Text(0, 18, ALIGN_FILL, ALIGN_SCREEN_CENTERED, "menu", text)); - addWidget(new Text(130, 50, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[map size x]"))); - addWidget(new Text(130, 75, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED, "standard", Toolkit::getStringTable()->getString("[map size y]"))); +void NewMapScreen::updateControls() +{ + terrains->visible = descriptor.method == MapGenerationDescriptor::eUNIFORM; + for (auto &widget : controlWidgets) + { + const auto &c = widget.definition; + bool size = + c.field == &MapGenerationDescriptor::wDec || c.field == &MapGenerationDescriptor::hDec; + 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); + } } void NewMapScreen::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==OK)||(par1==CANCEL)) + if (par1 == OK && !descriptor.hasTerrainWeight()) + MessageBox(globalContainer->gfx, "standard", MB_ONEBUTTON, + tr("Give at least one terrain type a nonzero weight."), tr("ok")); + else if (par1 == OK || par1 == CANCEL) endExecute(par1); } else if (action==NUMBER_ELEMENT_SELECTED) { - descriptor.wDec=mapSizeX->getNth()+6; - descriptor.hDec=mapSizeY->getNth()+6; - - // not eUNIFORM - descriptor.nbTeams=nbTeams->getNth()+1; - - // eRANDOM - descriptor.smooth=smooth->getNth()+1; - - // eOLDISLANDS - descriptor.oldBeach=oldBeach->getNth(); - descriptor.nbWorkers=nbWorkers->getNth()+1; - - descriptor.logRepeatAreaTimes=logRepeatAreaTimes->getNth(); - - // eISLANDS - descriptor.extraIslands=extraIslands->getNth(); + 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); } else if (action==LIST_ELEMENT_SELECTED) { - // eUNIFORM if (source==terrains) { - if (auto sel = terrains->selection()) - descriptor.terrainType = (TerrainType)*sel; + if (auto selection = terrains->selection()) + descriptor.terrainType = static_cast(*selection); } - - // all - if (source==methods) + else if (source == methods) { - auto sel = methods->selection(); - if (!sel) - return; - MapGenerationDescriptor::Method old=descriptor.method; - descriptor.method=(MapGenerationDescriptor::Method)*sel; - - if (old!=descriptor.method) + if (auto selection = methods->selection()) { - terrains->visible=false; - ratioText->visible=false; - waterRatio->visible=false; - waterText->visible=false; - sandRatio->visible=false; - sandText->visible=false; - grassRatio->visible=false; - grassText->visible=false; - desertRatio->visible=false; - desertText->visible=false; - smooth->visible=false; - smoothingText->visible=false; - wheatRatio->visible=false; - wheatText->visible=false; - woodRatio->visible=false; - woodText->visible=false; - stoneRatio->visible=false; - stoneText->visible=false; - algaeRatio->visible=false; - algaeText->visible=false; - riverDiameter->visible=false; - riverDiameterText->visible=false; - extraIslands->visible=false; - extraIslandsText->visible=false; - craterDensity->visible=false; - craterDensityText->visible=false; - riverDiameter->visible=false; - riverDiameterText->visible=false; - oldBeach->visible=false; - oldBeachSizeText->visible=false; - oldIslandSize->visible=false; - oldIslandSizeText->visible=false; - riverDiameter->visible=false; - riverDiameterText->visible=false; - fruitRatio->visible=false; - fruitText->visible=false; - logRepeatAreaTimes->visible=false; - areaTimesText->visible=false; - - // not eUNIFORM - nbTeams->setVisible(descriptor.method!=MapGenerationDescriptor::eUNIFORM); - nbWorkers->setVisible(descriptor.method!=MapGenerationDescriptor::eUNIFORM); - numberOfTeamText->setVisible(descriptor.method!=MapGenerationDescriptor::eUNIFORM); - numberOfWorkerText->setVisible(descriptor.method!=MapGenerationDescriptor::eUNIFORM); - - switch (descriptor.method) - { - case MapGenerationDescriptor::eUNIFORM: - terrains->visible=true; - break; - case MapGenerationDescriptor::eSWAMP: - ratioText->visible= - waterRatio->visible=waterText->visible= - grassRatio->visible=grassText->visible= - smooth->visible=smoothingText->visible= - wheatRatio->visible=wheatText->visible= - woodRatio->visible=woodText->visible= - stoneRatio->visible=stoneText->visible= - algaeRatio->visible=algaeText->visible= - fruitRatio->visible=fruitText->visible= - logRepeatAreaTimes->visible=areaTimesText->visible= - true; - break; - case MapGenerationDescriptor::eRIVER: - ratioText->visible= - waterRatio->visible=waterText->visible= - sandRatio->visible=sandText->visible= - grassRatio->visible=grassText->visible= - desertRatio->visible=desertText->visible= - smooth->visible=smoothingText->visible= - wheatRatio->visible=wheatText->visible= - woodRatio->visible=woodText->visible= - stoneRatio->visible=stoneText->visible= - algaeRatio->visible=algaeText->visible= - riverDiameter->visible=riverDiameterText->visible= - fruitRatio->visible=fruitText->visible= - logRepeatAreaTimes->visible=areaTimesText->visible= - true; - break; - case MapGenerationDescriptor::eISLANDS: - ratioText->visible= - waterRatio->visible=waterText->visible= - sandRatio->visible=sandText->visible= - grassRatio->visible=grassText->visible= - desertRatio->visible=desertText->visible= - smooth->visible=smoothingText->visible= - wheatRatio->visible=wheatText->visible= - woodRatio->visible=woodText->visible= - stoneRatio->visible=stoneText->visible= - algaeRatio->visible=algaeText->visible= - extraIslands->visible=extraIslandsText->visible= - fruitRatio->visible=fruitText->visible= - logRepeatAreaTimes->visible=areaTimesText->visible= - true; - break; - case MapGenerationDescriptor::eCRATERLAKES: - ratioText->visible= - waterRatio->visible=waterText->visible= - sandRatio->visible=sandText->visible= - grassRatio->visible=grassText->visible= - desertRatio->visible=desertText->visible= - smooth->visible=smoothingText->visible= - wheatRatio->visible=wheatText->visible= - woodRatio->visible=woodText->visible= - stoneRatio->visible=stoneText->visible= - algaeRatio->visible=algaeText->visible= - craterDensity->visible=craterDensityText->visible= - fruitRatio->visible=fruitText->visible= - logRepeatAreaTimes->visible=areaTimesText->visible= - true; - break; - case MapGenerationDescriptor::eCONCRETEISLANDS: - break; - case MapGenerationDescriptor::eISLES: - break; - case MapGenerationDescriptor::eOLDRANDOM: - ratioText->visible= - waterRatio->visible=waterText->visible= - sandRatio->visible=sandText->visible= - grassRatio->visible=grassText->visible= - smooth->visible=smoothingText->visible= - wheatRatio->visible=wheatText->visible= - woodRatio->visible=woodText->visible= - stoneRatio->visible=stoneText->visible= - algaeRatio->visible=algaeText->visible= - true; - break; - case MapGenerationDescriptor::eOLDISLANDS: - oldBeach->visible=oldBeachSizeText->visible= - oldIslandSize->visible=oldIslandSizeText->visible= - true; - break; - default: assert(false);break; - } + history.select(descriptor, + static_cast(*selection)); + updateControls(); } } } - else if (action==RATIO_CHANGED) - { - descriptor.waterRatio=waterRatio->get(); - descriptor.sandRatio=sandRatio->get(); - descriptor.grassRatio=grassRatio->get(); - descriptor.desertRatio=desertRatio->get(); - descriptor.wheatRatio=wheatRatio->get(); - descriptor.woodRatio=woodRatio->get(); - descriptor.algaeRatio=algaeRatio->get(); - descriptor.stoneRatio=stoneRatio->get(); - descriptor.fruitRatio=fruitRatio->get(); - descriptor.riverDiameter=riverDiameter->get(); - descriptor.craterDensity=craterDensity->get(); - //eISLANDS - descriptor.oldIslandSize=oldIslandSize->get(); - } } diff --git a/src/NewMapScreen.h b/src/NewMapScreen.h index a8ac51d70..211e9086d 100644 --- a/src/NewMapScreen.h +++ b/src/NewMapScreen.h @@ -27,21 +27,18 @@ class NewMapScreen : public Glob2Screen MapGenerationDescriptor descriptor; private: - Number *mapSizeX, *mapSizeY; - List *methods, *terrains; - Ratio *waterRatio, *sandRatio, *grassRatio, *desertRatio; - Ratio *wheatRatio, *woodRatio, *stoneRatio, *algaeRatio, *craterDensity; - Ratio *riverDiameter, *fruitRatio; - Number *smooth, *extraIslands; - Number *nbTeams; - Ratio *oldIslandSize; - Number *oldBeach; - Number *nbWorkers; - Number *logRepeatAreaTimes; - Text *numberOfTeamText, *numberOfWorkerText, *craterDensityText, *extraIslandsText; - Text *ratioText, *waterText, *sandText, *grassText, *desertText, *wheatText, *woodText, *stoneText, *algaeText, *fruitText, *smoothingText, *riverDiameterText, *areaTimesText; - Text *oldIslandSizeText, *oldBeachSizeText; - + 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: //! Constructor diff --git a/src/SConscript b/src/SConscript index 1cfb2b1e5..4960aa7a1 100644 --- a/src/SConscript +++ b/src/SConscript @@ -210,8 +210,10 @@ 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 @@ -566,6 +568,13 @@ if not env['server']: trapped_test = trapped_env.Program('TrappedUnitLifecycleTest', trapped_sources) local.Alias('trapped-unit-test', trapped_test) +# 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')]: + 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)) + #Add libgag, not as a library, but as an object server_source_files.append("../libgag/src/libgag_server.a") p2 = local.Program("glob2-server", server_source_files) diff --git a/src/map/Map.h b/src/map/Map.h index 6132d9380..2835d73fc 100644 --- a/src/map/Map.h +++ b/src/map/Map.h @@ -821,6 +821,8 @@ class Map 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/generator/GameMaps.cpp b/src/map/generator/GameMaps.cpp index debf00b9a..41c420f0f 100644 --- a/src/map/generator/GameMaps.cpp +++ b/src/map/generator/GameMaps.cpp @@ -25,7 +25,7 @@ bool Game::oldMakeIslandsMap(MapGenerationDescriptor &descriptor) map.setUMatPos(descriptor.bootX[s]+2, descriptor.bootY[s]+2, GRASS, squareSize); Sint32 typeNum=globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); - if (!checkRoomForBuilding(descriptor.bootX[s], descriptor.bootY[s], globalContainer->buildingsTypes.get(typeNum), -1, 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); @@ -83,4 +83,3 @@ bool Game::makeRandomMap(MapGenerationDescriptor &descriptor) } return true; } - diff --git a/src/map/generator/Generator.cpp b/src/map/generator/Generator.cpp index 2e32ce26b..2a271c0f6 100644 --- a/src/map/generator/Generator.cpp +++ b/src/map/generator/Generator.cpp @@ -13,13 +13,14 @@ #include "Unit.h" #include "Utilities.h" -bool MapGenerator::generateMap(Game& game, MapGenerationDescriptor &descriptor) +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); - setRandomSyncRandSeed(); + if (syncSeed) setSyncRandSeed(*syncSeed); + else setRandomSyncRandSeed(); switch (descriptor.method) { @@ -49,12 +50,14 @@ bool MapGenerator::generateMap(Game& game, MapGenerationDescriptor &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: @@ -72,6 +75,12 @@ bool MapGenerator::generateMap(Game& game, MapGenerationDescriptor &descriptor) 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; i5) + if (d > bridgeWidth + 1) { grid[ny * game.map.getW() + nx] = connectorArea; } @@ -341,8 +363,8 @@ bool MapGenerator::computeIsles(Game& game, MapGenerationDescriptor& descriptor) for(int y=0; y 1 && d <= 4) - heightmap[y * game.map.getW() + x] += (4-d)*33; + 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; } @@ -392,7 +414,7 @@ bool MapGenerator::computeIsles(Game& game, MapGenerationDescriptor& descriptor) { int d = distances[y * game.map.getW() + x]; int d2 = connectorDistances[y * game.map.getW() + x]; - if(d == 8 && d2 > 4) + if (d == 8 && d2 > bridgeWidth) { possible.push_back(MapGeneratorPoint(x, y)); } @@ -426,6 +448,3 @@ bool MapGenerator::computeIsles(Game& game, MapGenerationDescriptor& descriptor) } return true; } - - - diff --git a/src/map/generator/MapGenerationDescriptor.cpp b/src/map/generator/MapGenerationDescriptor.cpp index 069e0f415..76ece7442 100644 --- a/src/map/generator/MapGenerationDescriptor.cpp +++ b/src/map/generator/MapGenerationDescriptor.cpp @@ -2,44 +2,198 @@ // 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() { - wDec=7; - hDec=7; - terrainType=GRASS; - - method=eUNIFORM; - waterRatio=50; - sandRatio=50; - grassRatio=50; - desertRatio=50; - wheatRatio=50; - woodRatio=50; - stoneRatio=50; - algaeRatio=50; - riverDiameter=50; - craterDensity=50; - extraIslands=0; - smooth=4; - fruitRatio=4; - logRepeatAreaTimes=0; - - oldIslandSize=50; - oldBeach=1; - for (int i=0; i 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() { @@ -216,4 +370,3 @@ Uint32 MapGenerationDescriptor::checkSum() return cs; } - diff --git a/src/map/generator/MapGenerationDescriptor.h b/src/map/generator/MapGenerationDescriptor.h index e9cf9d968..c5e1c5f53 100644 --- a/src/map/generator/MapGenerationDescriptor.h +++ b/src/map/generator/MapGenerationDescriptor.h @@ -6,6 +6,8 @@ #include "Ressource.h" #include "TerrainType.h" #include "Team.h" +#include +#include namespace GAGCore { @@ -48,6 +50,34 @@ class MapGenerationDescriptor 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; @@ -56,6 +86,9 @@ class MapGenerationDescriptor 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. @@ -64,11 +97,22 @@ class MapGenerationDescriptor Sint32 nbTeams, nbWorkers; public: // Those may not be in data - Sint32 bootX[Team::MAX_COUNT]; - Sint32 bootY[Team::MAX_COUNT]; + 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]; + 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); }; diff --git a/src/map/generator/MapGenerator.h b/src/map/generator/MapGenerator.h index f45bdcc98..57973c4b2 100644 --- a/src/map/generator/MapGenerator.h +++ b/src/map/generator/MapGenerator.h @@ -4,6 +4,8 @@ #pragma once +#include + #include "MapGenerationDescriptor.h" class MapGeneratorPoint; @@ -14,7 +16,10 @@ class MapGenerator { public: ///Generates a map from the given generation description - bool generateMap(Game& game, MapGenerationDescriptor& descriptor); + // 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); diff --git a/src/map/generator/MapOldIslandsRessources.cpp b/src/map/generator/MapOldIslandsRessources.cpp new file mode 100644 index 000000000..1758875d9 --- /dev/null +++ b/src/map/generator/MapOldIslandsRessources.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière + +#include + +#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/MapOldRandomRessources.cpp b/src/map/generator/MapOldRandomRessources.cpp new file mode 100644 index 000000000..b7b8f8b56 --- /dev/null +++ b/src/map/generator/MapOldRandomRessources.cpp @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière + +#include + +#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(method); + descriptor.setMethodDefaults(static_cast(method)); generated = generator.generateMap(game, descriptor) && game.teamsCount() == 4; } assert(generated); @@ -790,7 +829,7 @@ int main(int argc, char **argv) Game g(nullptr); MapGenerator generator; MapGenerationDescriptor d; - d.method = MapGenerationDescriptor::eRIVER; + d.setMethodDefaults(MapGenerationDescriptor::eRIVER); d.nbTeams = 4; assert(generator.generateMap(g, d)); assert(g.teamsCount() == 4); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp new file mode 100644 index 000000000..038ff8ccb --- /dev/null +++ b/test/MapGeneratorDefaultsTest.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#define SDL_MAIN_HANDLED +#include "GlobalContainer.h" +#include "NewMapScreen.h" +#include "CustomGameSetup.h" +#include +#include +#include +#include +#include +#include +#include +#include + +GlobalContainer *globalContainer = nullptr; +using D = MapGenerationDescriptor; + +class MapGeneratorDefaultsTest +{ + public: + static void select(NewMapScreen &s, D::Method method) + { + s.methods->setSelectionIndex(method); + s.onAction(s.methods, GAGGUI::LIST_ELEMENT_SELECTED, method, 0); + } + static void sameControls(const D &a, const D &b) + { + assert(a.method == b.method); + for (const auto &c : D::controls(a.method)) + assert(c.get(a) == c.get(b)); + for (const auto &c : D::sharedControls()) + assert(c.get(a) == c.get(b)); + } + 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) + { + w.number->setNth((value - w.definition.minimum) / w.definition.step); + s.onAction(w.number, GAGGUI::NUMBER_ELEMENT_SELECTED, 0, 0); + assert(w.definition.get(s.descriptor) == value); + return; + } + assert(false); + } + static void run(const char *output) + { + NewMapScreen s; + s.gfx = globalContainer->gfx; + s.dispatchInit(); + CustomGameSetup lobby; + D river; + river.setMethodDefaults(D::eRIVER); + sameControls(lobby.generator, river); + for (int m = 0; m <= D::eOLDISLANDS; ++m) + { + auto method = static_cast(m); + D expected; + expected.setMethodDefaults(method); + select(s, method); + lobby.generatorHistory.select(lobby.generator, method); + sameControls(s.descriptor, expected); + sameControls(lobby.generator, expected); + s.dispatchTimer(1); + sameControls(s.descriptor, expected); + std::set labels; + for (const auto &c : D::controls(method)) + { + assert(labels.insert(c.label).second); + assert(c.step > 0 && c.maximum >= c.minimum); + assert((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) + 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 (output && m >= 4 && m <= 8) + { + s.gfx->drawFilledRect(0, 0, 640, 480, GAGCore::Color(34, 55, 42)); + for (auto *w : s.widgets) + if (w->visible) + w->paint(); + std::string path = std::string(output) + "/editor-" + std::to_string(m) + ".png"; + assert(IMG_SavePNG(s.gfx->getSDLSurface(), path.c_str()) == 0); + } + } + select(s, D::eRIVER); + edit(s, "Water weight", 37); + edit(s, "Width", 8); + edit(s, "Colonies", 6); + lobby.generatorHistory.select(lobby.generator, D::eRIVER); + lobby.generator.waterRatio = 37; + lobby.generator.wDec = 8; + lobby.generator.nbTeams = 6; + for (auto method : {D::eISLANDS, D::eISLES, D::eRIVER}) + { + select(s, method); + lobby.generatorHistory.select(lobby.generator, method); + sameControls(s.descriptor, lobby.generator); + assert(s.descriptor.wDec == 8 && s.descriptor.nbTeams == 6); + } + assert(s.descriptor.waterRatio == 37); + lobby.random = true; + lobby.generator.waterRatio = lobby.generator.grassRatio = lobby.generator.sandRatio = + lobby.generator.desertRatio = 0; + assert(!lobby.validation().empty()); + lobby.generator.grassRatio = 75; + assert(lobby.validation().empty()); + puts("PASS shared presets, ranges and steps; all editor values; lobby/editor mode memory; " + "serialization and validation"); + } +}; +int main(int argc, char **argv) +{ + assert(argc == 2 || argc == 3); + SDL_SetMainReady(); + SDL_setenv("SDL_VIDEODRIVER", "dummy", 1); + SDL_setenv("SDL_AUDIODRIVER", "dummy", 1); + GlobalContainer globals(argv[1]); + globalContainer = &globals; + globals.runNoX = true; + globals.load(); + globals.gfx = GAGCore::Toolkit::initGraphic(640, 480, 0, "Map defaults test", ""); + GAGCore::Toolkit::loadFont("data/fonts/sans.ttf", 20, "menu"); + GAGCore::Toolkit::loadFont("data/fonts/sans.ttf", 13, "standard"); + MapGeneratorDefaultsTest::run(argc == 3 ? argv[2] : nullptr); +} diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp new file mode 100644 index 000000000..6c643032c --- /dev/null +++ b/test/MapGeneratorStudy.cpp @@ -0,0 +1,278 @@ +// 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 "IntBuildingType.h" +#include "Race.h" +#include "PerlinNoise.h" +#include "Utilities.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Unit.h" + +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; + std::puts("["); + for (int m = 0; m <= D::eOLDISLANDS; ++m) + { + auto method = static_cast(m); + std::printf("{\"method\":%d,\"nameKey\":\"%s\",\"controls\":[", m, + D::methodName(method)); + auto controls = D::sharedControls(); + const auto &specific = D::controls(method); + controls.insert(controls.end(), specific.begin(), specific.end()); + 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\n", m == D::eOLDISLANDS ? "" : ","); + } + std::puts("]"); + return 0; + } + if (argc < 4) + 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; + globals.runNoX = true; + globals.settings.rememberUnit = false; + globals.buildingsTypes.init(); + IntBuildingType::init(); + Race::loadDefault(); + Game game(nullptr); + MapGenerationDescriptor descriptor; + descriptor.method = static_cast(method); + bool terrainOnly = false; + 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}}; + 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) + { + auto key = arg.substr(0, eq); + if (key == "dump") + dump = arg.substr(eq + 1); + else if (controls.count(key)) + *controls[key] = std::stoi(arg.substr(eq + 1)); + else + return 2; + } + } + 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 double seconds = + std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + auto &map = game.map; + int grass = 0, sand = 0, water = 0, shore = 0, free = 0, fit4 = 0; + int umGrass = 0, umSand = 0, umWater = 0; + std::uint64_t hash = 14695981039346656037ULL; + std::vector footprint(map.getW() * map.getH(), 0); + int resources[8] = {}; + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) + { + if (map.isGrass(x, y)) + ++grass; + else if (map.isSand(x, y)) + ++sand; + else if (map.isWater(x, y)) + ++water; + else + ++shore; + if (map.isFreeForBuilding(x, y)) + ++free; + if (map.isFreeForBuilding(x, y, 4, 4)) + { + ++fit4; + footprint[y * map.getW() + x] = 1; + } + if (map.getResource(x, y).type < 8) + ++resources[map.getResource(x, y).type]; + switch (map.getUMTerrain(x, y)) + { + case GRASS: + ++umGrass; + break; + case SAND: + ++umSand; + break; + case WATER: + ++umWater; + break; + } + hash ^= map.getTerrain(x, y); + hash *= 1099511628211ULL; + hash ^= map.getResource(x, y).getUint32(); + hash *= 1099511628211ULL; + } + std::printf("STUDY,%d,%u,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%.6f,%llu\n", method, seed, success, + map.getW() * map.getH(), grass, sand, water, shore, free, fit4, umGrass, umSand, + umWater, seconds, (unsigned long long)hash); + if (tuning) + { + int minLocal = map.getW() * map.getH(), minWheat = 100000, minWood = 100000, + viableTeams = 0; + if (success && method != 0) + for (int t = 0; t < game.teamsCount(); ++t) + { + std::vector dist(map.getW() * map.getH(), -1); + std::queue q; + // Seed the flood from actual starting workers, not a guessed base edge. + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) + { + auto gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == t) + { + int p = y * map.getW() + x; + dist[p] = 0; + q.push(p); + } + } + int local = 0, wheat = 100000, wood = 100000; + while (!q.empty()) + { + int p = q.front(); + q.pop(); + int x = p % map.getW(), y = p / map.getW(); + // Expansion space within 24 walking steps of a starting worker. + if (dist[p] <= 24 && footprint[p]) + ++local; + 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), + np = ny * map.getW() + nx; + auto type = map.getResource(nx, ny).type; + if (type == CORN) + wheat = std::min(wheat, dist[p] + 1); + if (type == WOOD) + wood = std::min(wood, dist[p] + 1); + if (dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + { + dist[np] = dist[p] + 1; + q.push(np); + } + } + } + minLocal = std::min(minLocal, local); + // Store the worst (largest) resource distance across teams below. + if (t == 0) + { + minWheat = wheat; + minWood = wood; + } + else + { + minWheat = std::max(minWheat, wheat); + minWood = std::max(minWood, 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]); + } + if (!dump.empty()) + { + FILE *f = std::fopen(dump.c_str(), "w"); + if (!f) + return 3; + std::fprintf(f, "%d %d\n", map.getW(), map.getH()); + for (int y = 0; y < map.getH(); ++y) + { + for (int x = 0; x < map.getW(); ++x) + { + int c = map.isGrass(x, y) ? 0 : map.isSand(x, y) ? 1 : map.isWater(x, y) ? 2 : 3; + if (map.getResource(x, y).type == CORN) + c = 4; + if (map.getResource(x, y).type == WOOD) + c = 5; + if (map.getResource(x, y).type == STONE) + c = 6; + if (map.getBuilding(x, y) != NOGBID) + c = 7; + std::fprintf(f, "%d ", c); + } + std::fprintf(f, "\n"); + } + std::fclose(f); + } + return 0; +} diff --git a/tools/map_generator_study.py b/tools/map_generator_study.py new file mode 100644 index 000000000..33f7951e5 --- /dev/null +++ b/tools/map_generator_study.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Bounded parameter search with fresh-process samples and held-out seeds.""" +import argparse, atexit, concurrent.futures, csv, itertools, json, os, random, shutil, subprocess, threading, time +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} +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() +atexit.register(lambda:[shutil.rmtree(Path.home()/('.'+p),ignore_errors=True) for p in PROFILES]) + +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'] + command += [f'{k}={v}' for k,v in params.items()] + if 'dump' in config: command += ['dump='+config['dump']] + start=time.monotonic() + try: + result=subprocess.run(command,cwd=ROOT,text=True,capture_output=True,timeout=12) + lines=result.stdout.splitlines() + a=[x for x in lines if x.startswith('STUDY,')] + 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:])) + 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:] + except subprocess.TimeoutExpired: + status='timeout';detail='12-second timeout' + (OUT/f'error-{config["id"]}-{seed}.txt').write_text(detail) + return {'config':config['id'],'method':config['method'],'seed':seed,'success':0,'status':status,'seconds':time.monotonic()-start} + +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]) + 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) + tasks=[(c,s) for s in range(start,start+count) for c in configs] + begun=time.monotonic() + (OUT/(label+'-configs.json')).write_text(json.dumps(configs,indent=2)) + with (OUT/(label+'.csv')).open('w') as f, concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool: + writer=csv.DictWriter(f,['config']+FIELDS+EXTRA+['status']);writer.writeheader() + for i,row in enumerate(pool.map(sample,tasks)): + writer.writerow(row);f.flush() + if (i+1)%500==0:print(f'{label}: {i+1}/{len(tasks)}, {time.monotonic()-begun:.0f}s',flush=True) + 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()) + run(c,a.count,a.start,a.label,a.workers) diff --git a/tools/plot_map_generator_study.py b/tools/plot_map_generator_study.py new file mode 100644 index 000000000..edb513986 --- /dev/null +++ b/tools/plot_map_generator_study.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Publish a reproducible coverage report using the compiled generator catalog. + +Usage: python3 tools/plot_map_generator_study.py path/to/validation.csv +Requires numpy and matplotlib. Run after building map-generator-study. +""" +import collections +import csv +import gzip +import json +from pathlib import Path +import shutil +import subprocess +import sys +import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +from matplotlib.colors import ListedColormap +from matplotlib.patches import Patch + +ROOT=Path(__file__).resolve().parents[1] +OUT=ROOT/'artifacts/map-generators' +STUDY=OUT/'study' +BINARY=ROOT/'build/src/MapGeneratorStudy' +catalog=json.loads(subprocess.check_output([BINARY,'--catalog'],text=True)) +(STUDY/'catalog.json').write_text(json.dumps(catalog,indent=2)+'\n') +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(']')} +names={c['method']:strings[c['nameKey']] for c in catalog} +source=Path(sys.argv[1]) +groups=collections.defaultdict(list) +for row in csv.DictReader(source.open()):groups[row['config']].append(row) +summary=[] +for key,rows in groups.items(): + ok=[r for r in rows if r['success']=='1'];m=int(rows[0]['method']) + record=dict(config=key,method=m,name=names[m],attempts=len(rows),successes=len(ok),all_teams_proxy=sum(r['viable_teams']=='4' for r in ok)) + for field in ['grass_tiles','free','fit4','water_tiles','sand_tiles','shore']: + values=np.array([100*int(r[field])/int(r['tiles']) for r in ok]) + record[field]=float(values.mean());record[field+'_p5']=float(np.percentile(values,5));record[field+'_p95']=float(np.percentile(values,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) + if m in [7,8]:assert all(int(r['wheat_tiles'])>0 and int(r['wood_tiles'])>0 for r in ok) + summary.append(record) +(STUDY/'summary.json').write_text(json.dumps(summary,indent=2)+'\n') +with (STUDY/'summary.csv').open('w') as f: + w=csv.DictWriter(f,summary[0].keys());w.writeheader();w.writerows(summary) +with source.open('rb') as src, (STUDY/'validation.csv.gz').open('wb') as dest: + with gzip.GzipFile(fileobj=dest,mode='wb',mtime=0,filename='') as compressed:shutil.copyfileobj(src,compressed) +bykey={r['config']:r for r in summary} +fig,axes=plt.subplots(1,2,figsize=(12,6),sharey=True,facecolor='#f8fafc') +for ax,field,title in zip(axes,['grass_tiles','free'],['Pure grass terrain','Immediately buildable tiles']): + for prefix,offset,color,label in [('lobby-before',-.18,'#afbdc9','PR #237 lobby defaults'),('tuned',.18,'#38835c','Tuned defaults')]: + vals=[bykey[f'{prefix}-{m}'][field] for m in range(1,9)] + bars=ax.barh(np.arange(8)+offset,vals,.34,color=color,label=label) + ax.bar_label(bars,labels=[f'{v:.1f}%' for v in vals],padding=4,fontsize=9) + ax.set_title(title,fontweight='bold');ax.set_xlim(0,78);ax.set_xlabel('% of map tiles');ax.grid(axis='x',alpha=.15);ax.set_axisbelow(True) + for sp in ax.spines.values():sp.set_visible(False) +axes[0].set_yticks(range(8),[names[m] for m in range(1,9)]);axes[0].invert_yaxis();axes[0].legend(loc='lower right',fontsize=9) +fig.suptitle('Tuned generator defaults: more building space',fontsize=17,fontweight='bold') +fig.text(.02,.015,'1,000 fixed seed attempts per generator per cohort · 128×128 · 4 teams · means exclude generation failures\nBoth cohorts include the legacy correctness fixes. Failure counts and the previous editor baseline are in RESULTS.md.',fontsize=9,color='#4b5563') +fig.tight_layout(rect=(0,.07,1,.94)) +for ext in ['png','svg']:fig.savefig(OUT/f'coverage.{ext}',dpi=160) +plt.close(fig) +svg=OUT/'coverage.svg' +svg.write_text('\n'.join(line.rstrip() for line in svg.read_text().splitlines())+'\n') +report=['# Measured generator defaults','','Generated from the compiled catalog and fixed seed results. Settings below are a documentation snapshot; change the C++ definitions, then regenerate this file.','','## Coverage and reliability','','All cohorts use the same 1,000 seed attempts per mode (20001–21000); failed attempts are not replaced. Percentages are means of successful maps. Both prior-setting cohorts include the legacy fixes.','','| Generator | Grass: lobby → tuned | Free tiles: lobby → tuned | Failures: lobby → tuned | All-team start proxy: lobby → tuned |','|---|---:|---:|---:|---:|'] +for m in range(1,9): + a,b=bykey[f'lobby-before-{m}'],bykey[f'tuned-{m}'] + report.append(f"| {names[m]} | {a['grass_tiles']:.1f}% → {b['grass_tiles']:.1f}% | {a['free']:.1f}% → {b['free']:.1f}% | {1000-a['successes']} → {1000-b['successes']} | {a['all_teams_proxy']} → {b['all_teams_proxy']} |") +report+=['','## Previous editor comparison','','The legacy editor normally applies zero sand/desert weights on its first visible slider timer tick. PR #237’s new lobby used constructor weights of 50 for both. The two previous-setting cohorts are intentionally distinct.','','| Generator | Editor free tiles → tuned | Editor failures → tuned |','|---|---:|---:|'] +for m in range(1,9): + a,b=bykey[f'editor-before-{m}'],bykey[f'tuned-{m}'] + report.append(f"| {names[m]} | {a['free']:.1f}% → {b['free']:.1f}% | {1000-a['successes']} → {1000-b['successes']} |") +report+=['','## Shared controls','','| Control | Range | Step | Default |','|---|---|---:|---:|'] +def row(c): + def value(v):return 1<