268 lines
9.7 KiBLFS
Python
268 lines
9.7 KiBLFS
Python
#!/usr/bin/env python3
|
|
"""
|
|
Civ6Map to Scenario Converter
|
|
=============================
|
|
|
|
Converts WorldBuilder-exported .Civ6Map files (SQLite databases) to
|
|
scenario.json format for the civ6-optimizer task.
|
|
|
|
.Civ6Map files are SQLite databases. Plot coordinates are derived from
|
|
plot ID: x = ID % width, y = ID // width
|
|
|
|
Usage:
|
|
python tools/civ6map_to_scenario.py map.Civ6Map -o scenario.json
|
|
python tools/civ6map_to_scenario.py map.Civ6Map --population 7 --districts CAMPUS,HARBOR
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sqlite3
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Tuple
|
|
|
|
|
|
# Terrain mappings
|
|
TERRAIN_MAP = {
|
|
"TERRAIN_COAST": "COAST",
|
|
"TERRAIN_OCEAN": "OCEAN",
|
|
"TERRAIN_GRASS": "GRASS",
|
|
"TERRAIN_GRASS_HILLS": "GRASS",
|
|
"TERRAIN_GRASS_MOUNTAIN": "MOUNTAIN",
|
|
"TERRAIN_PLAINS": "PLAINS",
|
|
"TERRAIN_PLAINS_HILLS": "PLAINS",
|
|
"TERRAIN_PLAINS_MOUNTAIN": "MOUNTAIN",
|
|
"TERRAIN_DESERT": "DESERT",
|
|
"TERRAIN_DESERT_HILLS": "DESERT",
|
|
"TERRAIN_DESERT_MOUNTAIN": "MOUNTAIN",
|
|
"TERRAIN_TUNDRA": "TUNDRA",
|
|
"TERRAIN_TUNDRA_HILLS": "TUNDRA",
|
|
"TERRAIN_TUNDRA_MOUNTAIN": "MOUNTAIN",
|
|
"TERRAIN_SNOW": "SNOW",
|
|
"TERRAIN_SNOW_HILLS": "SNOW",
|
|
"TERRAIN_SNOW_MOUNTAIN": "MOUNTAIN",
|
|
}
|
|
|
|
FEATURE_MAP = {
|
|
"FEATURE_FOREST": "FEATURE_FOREST",
|
|
"FEATURE_JUNGLE": "FEATURE_JUNGLE",
|
|
"FEATURE_MARSH": "FEATURE_MARSH",
|
|
"FEATURE_OASIS": "FEATURE_OASIS",
|
|
"FEATURE_REEF": "FEATURE_REEF",
|
|
"FEATURE_ICE": "FEATURE_ICE",
|
|
"FEATURE_FLOODPLAINS": "FEATURE_FLOODPLAINS",
|
|
"FEATURE_FLOODPLAINS_GRASSLAND": "FEATURE_FLOODPLAINS",
|
|
"FEATURE_FLOODPLAINS_PLAINS": "FEATURE_FLOODPLAINS",
|
|
"FEATURE_GEOTHERMAL_FISSURE": "FEATURE_GEOTHERMAL_FISSURE",
|
|
"FEATURE_VOLCANIC_SOIL": "FEATURE_VOLCANIC_SOIL",
|
|
}
|
|
|
|
NATURAL_WONDERS = {
|
|
"FEATURE_BARRIER_REEF", "FEATURE_CLIFFS_DOVER", "FEATURE_CRATER_LAKE",
|
|
"FEATURE_DEAD_SEA", "FEATURE_EVEREST", "FEATURE_GALAPAGOS",
|
|
"FEATURE_KILIMANJARO", "FEATURE_PANTANAL", "FEATURE_PIOPIOTAHI",
|
|
"FEATURE_TORRES_DEL_PAINE", "FEATURE_TSINGY", "FEATURE_YOSEMITE",
|
|
"FEATURE_ULURU", "FEATURE_DELICATE_ARCH", "FEATURE_EYE_OF_THE_SAHARA",
|
|
"FEATURE_LAKE_RETBA", "FEATURE_MATTERHORN", "FEATURE_RORAIMA",
|
|
"FEATURE_UBSUNUR_HOLLOW", "FEATURE_ZHANGYE_DANXIA", "FEATURE_HA_LONG_BAY",
|
|
"FEATURE_EYJAFJALLAJOKULL", "FEATURE_LYSEFJORDEN", "FEATURE_GIANTS_CAUSEWAY",
|
|
}
|
|
|
|
RESOURCE_TYPE_MAP = {
|
|
"RESOURCE_HORSES": "STRATEGIC", "RESOURCE_IRON": "STRATEGIC",
|
|
"RESOURCE_NITER": "STRATEGIC", "RESOURCE_COAL": "STRATEGIC",
|
|
"RESOURCE_OIL": "STRATEGIC", "RESOURCE_ALUMINUM": "STRATEGIC",
|
|
"RESOURCE_URANIUM": "STRATEGIC",
|
|
"RESOURCE_DIAMONDS": "LUXURY", "RESOURCE_PEARLS": "LUXURY",
|
|
"RESOURCE_JADE": "LUXURY", "RESOURCE_MARBLE": "LUXURY",
|
|
"RESOURCE_SILK": "LUXURY", "RESOURCE_SPICES": "LUXURY",
|
|
"RESOURCE_SUGAR": "LUXURY", "RESOURCE_TEA": "LUXURY",
|
|
"RESOURCE_TOBACCO": "LUXURY", "RESOURCE_WINE": "LUXURY",
|
|
"RESOURCE_WHALES": "LUXURY", "RESOURCE_COTTON": "LUXURY",
|
|
"RESOURCE_FISH": "BONUS", "RESOURCE_WHEAT": "BONUS",
|
|
"RESOURCE_CATTLE": "BONUS", "RESOURCE_SHEEP": "BONUS",
|
|
"RESOURCE_DEER": "BONUS", "RESOURCE_BANANAS": "BONUS",
|
|
"RESOURCE_RICE": "BONUS", "RESOURCE_MAIZE": "BONUS",
|
|
"RESOURCE_STONE": "BONUS", "RESOURCE_COPPER": "BONUS",
|
|
}
|
|
|
|
IMPROVEMENT_MAP = {
|
|
"IMPROVEMENT_MINE": "MINE",
|
|
"IMPROVEMENT_QUARRY": "QUARRY",
|
|
"IMPROVEMENT_LUMBER_MILL": "LUMBER_MILL",
|
|
"IMPROVEMENT_FARM": "FARM",
|
|
}
|
|
|
|
|
|
def id_to_coords(plot_id: int, width: int) -> Tuple[int, int]:
|
|
"""Convert plot ID to (x, y) coordinates."""
|
|
return plot_id % width, plot_id // width
|
|
|
|
|
|
def convert_civ6map(db_path: str) -> Dict[str, Any]:
|
|
"""Convert a .Civ6Map SQLite database to scenario dict."""
|
|
conn = sqlite3.connect(db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
cursor = conn.cursor()
|
|
|
|
# Get map dimensions
|
|
cursor.execute("SELECT Width, Height FROM Map LIMIT 1")
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
raise ValueError("Could not read map dimensions")
|
|
width, height = row["Width"], row["Height"]
|
|
print(f"Map: {width} x {height}", file=sys.stderr)
|
|
|
|
tiles: Dict[int, Dict[str, Any]] = {}
|
|
|
|
# Get available columns
|
|
cursor.execute("PRAGMA table_info(Plots)")
|
|
columns = {col["name"] for col in cursor.fetchall()}
|
|
|
|
# Query plots
|
|
cursor.execute("SELECT * FROM Plots")
|
|
for row in cursor.fetchall():
|
|
plot_id = row["ID"]
|
|
x, y = id_to_coords(plot_id, width)
|
|
|
|
raw_terrain = row["TerrainType"] or ""
|
|
terrain = TERRAIN_MAP.get(raw_terrain, raw_terrain.replace("TERRAIN_", ""))
|
|
is_hills = "_HILLS" in raw_terrain
|
|
|
|
tile: Dict[str, Any] = {"x": x, "y": y, "terrain": terrain}
|
|
if is_hills:
|
|
tile["is_hills"] = True
|
|
|
|
# Features
|
|
if "FeatureType" in columns and row["FeatureType"]:
|
|
raw_feature = row["FeatureType"]
|
|
if raw_feature in NATURAL_WONDERS:
|
|
tile["feature"] = raw_feature
|
|
tile["is_natural_wonder"] = True
|
|
elif raw_feature in FEATURE_MAP:
|
|
tile["feature"] = FEATURE_MAP[raw_feature]
|
|
else:
|
|
tile["feature"] = raw_feature
|
|
if "FLOODPLAINS" in raw_feature:
|
|
tile["is_floodplains"] = True
|
|
|
|
tiles[plot_id] = tile
|
|
|
|
print(f"Plots: {len(tiles)}", file=sys.stderr)
|
|
|
|
# Features from PlotFeatures table (if exists)
|
|
try:
|
|
cursor.execute("SELECT * FROM PlotFeatures")
|
|
feat_count = 0
|
|
for row in cursor.fetchall():
|
|
plot_id = row["PlotID"] if "PlotID" in dict(row).keys() else row["ID"]
|
|
if plot_id in tiles and "feature" not in tiles[plot_id]:
|
|
feature = row["FeatureType"]
|
|
if feature in NATURAL_WONDERS:
|
|
tiles[plot_id]["feature"] = feature
|
|
tiles[plot_id]["is_natural_wonder"] = True
|
|
elif feature in FEATURE_MAP:
|
|
tiles[plot_id]["feature"] = FEATURE_MAP[feature]
|
|
else:
|
|
tiles[plot_id]["feature"] = feature
|
|
# Set is_floodplains flag
|
|
if "FLOODPLAINS" in feature:
|
|
tiles[plot_id]["is_floodplains"] = True
|
|
feat_count += 1
|
|
print(f"Features: {feat_count}", file=sys.stderr)
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
# Rivers
|
|
# River edges are stored in two ways:
|
|
# 1. Is*OfRiver flags: river on NE (edge 1), NW (edge 2), W (edge 3) edges
|
|
# 2. FlowDirection values: river on E (edge 0), SW (edge 4), SE (edge 5) edges
|
|
# FlowDirection = -1 means no river, any other value means river present
|
|
try:
|
|
cursor.execute("SELECT * FROM PlotRivers")
|
|
river_count = 0
|
|
for row in cursor.fetchall():
|
|
row_dict = dict(row)
|
|
plot_id = row_dict.get("PlotID") or row_dict.get("ID")
|
|
if plot_id not in tiles:
|
|
continue
|
|
river_edges = []
|
|
# Check Is*OfRiver flags (edges 1, 2, 3)
|
|
if row_dict.get("IsNEOfRiver"):
|
|
river_edges.append(1)
|
|
if row_dict.get("IsNWOfRiver"):
|
|
river_edges.append(2)
|
|
if row_dict.get("IsWOfRiver"):
|
|
river_edges.append(3)
|
|
# Check FlowDirection values (edges 0, 4, 5) - value != -1 means river present
|
|
if row_dict.get("EFlowDirection", -1) != -1:
|
|
river_edges.append(0)
|
|
if row_dict.get("SWFlowDirection", -1) != -1:
|
|
river_edges.append(4)
|
|
if row_dict.get("SEFlowDirection", -1) != -1:
|
|
river_edges.append(5)
|
|
if river_edges:
|
|
tiles[plot_id]["river_edges"] = sorted(river_edges)
|
|
river_count += 1
|
|
print(f"Rivers: {river_count} tiles", file=sys.stderr)
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
# Resources
|
|
try:
|
|
cursor.execute("SELECT * FROM PlotResources")
|
|
res_count = 0
|
|
for row in cursor.fetchall():
|
|
plot_id = row["PlotID"] if "PlotID" in dict(row).keys() else row["ID"]
|
|
if plot_id in tiles:
|
|
resource = row["ResourceType"]
|
|
tiles[plot_id]["resource"] = resource
|
|
tiles[plot_id]["resource_type"] = RESOURCE_TYPE_MAP.get(resource, "BONUS")
|
|
res_count += 1
|
|
print(f"Resources: {res_count}", file=sys.stderr)
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
conn.close()
|
|
|
|
tiles_list = sorted(tiles.values(), key=lambda t: (t["y"], t["x"]))
|
|
return {"dimensions": {"width": width, "height": height}, "tiles": tiles_list}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Convert .Civ6Map to scenario JSON")
|
|
parser.add_argument("map_file", help="Path to .Civ6Map file")
|
|
parser.add_argument("-o", "--output", help="Output JSON file")
|
|
parser.add_argument("--id", default="scenario_1", help="Scenario ID")
|
|
parser.add_argument("--num-cities", type=int, default=1)
|
|
parser.add_argument("--population", type=int, default=7)
|
|
parser.add_argument("--civilization", default="GENERIC")
|
|
parser.add_argument("--districts", help="(deprecated) Districts to include in solution")
|
|
|
|
args = parser.parse_args()
|
|
|
|
result = convert_civ6map(args.map_file)
|
|
|
|
scenario = {
|
|
"id": args.id,
|
|
"description": f"Scenario from {Path(args.map_file).name}",
|
|
"dimensions": result["dimensions"],
|
|
"num_cities": args.num_cities,
|
|
"population": args.population,
|
|
"civilization": args.civilization,
|
|
"tiles": result["tiles"],
|
|
}
|
|
|
|
output_json = json.dumps(scenario, indent=2)
|
|
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
f.write(output_json)
|
|
print(f"Saved: {args.output}", file=sys.stderr)
|
|
else:
|
|
print(output_json)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|