feat(@projects/@magic-civilization): enhance player boot with real map generation

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-11 00:56:40 -07:00
parent cb78384088
commit df9f5c362c

View file

@ -80,25 +80,40 @@ func _bootstrap_game(
func _hydrate_player_api(num_players: int) -> void:
## Build a deterministic `GdGameState` with `num_players` empty-axis
## players + one militarist capital each, serialise it to JSON, and
## load it into `GdPlayerApi`.
##
## The grid created by `create_grid(40, 24)` is biome-empty (all tiles
## default to no biome string), so there is no ocean/coast hex to
## land on — capitals dropped at fixed offsets are safe.
## TRACKED: p2-67 Phase 1 follow-up — when the harness grows real
## `GdMapGenerator.generate()` integration, swap to a land-aware
## placement search like `gameplay_arc_proof::_find_land_tile_near`.
## Build a deterministic seeded `GdGameState` with a real generated
## map, then place `num_players` militarist capitals on land tiles
## found via `_find_land_tile_near`. Serialise + load into
## `GdPlayerApi`. Mirrors the production game's boot path:
## `GdMapGenerator.generate` → `GdGameState.set_grid_from_gridstate`
## → per-player land-aware capital placement.
var gs: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted
if gs == null:
_emit_protocol_error("ClassDB.instantiate('GdGameState') returned null")
return
gs.create_grid(40, 24)
var capital_col: int = 2
for pi: int in num_players:
var row: int = 2 + (pi * 2)
gs.add_player_militarist(capital_col + (pi * 2), row)
# Generate a real seeded map via GdMapGenerator.
var gen: RefCounted = ClassDB.instantiate("GdMapGenerator") as RefCounted
if gen == null:
_emit_protocol_error("ClassDB.instantiate('GdMapGenerator') returned null")
return
gen.initialize("{}")
var seed_v: int = _env_int("CP_SEED", 42)
var map_size: String = _env_or("CP_MAP_SIZE", "duel")
var grid: RefCounted = gen.generate(seed_v, map_size) as RefCounted
if grid == null:
_emit_protocol_error("GdMapGenerator.generate returned null")
return
gs.set_grid_from_gridstate(grid)
var grid_w: int = int(grid.get_width())
var grid_h: int = int(grid.get_height())
# Pre-compute land-tile set so we can pick capitals.
var land_tiles: Array[Vector2i] = _scan_land_tiles(grid, grid_w, grid_h)
if land_tiles.is_empty():
_emit_protocol_error("no land tiles in generated map — bad seed/params")
return
# Pick `num_players` capitals well-spaced across the land set.
var capitals: Array[Vector2i] = _pick_spaced_capitals(land_tiles, num_players)
for cap: Vector2i in capitals:
gs.add_player_militarist(cap.x, cap.y)
var json: String = String(gs.to_json())
if json.is_empty() or json == "{}":
_emit_protocol_error("GdGameState.to_json returned empty payload")
@ -107,6 +122,53 @@ func _hydrate_player_api(num_players: int) -> void:
_emit_protocol_error("GdPlayerApi.load_state_json rejected bootstrap state")
func _scan_land_tiles(grid: RefCounted, w: int, h: int) -> Array[Vector2i]:
## Walk the grid and collect every land hex. Mirrors
## `mc_mapgen::spawn_box::FORBIDDEN_BIOMES` pathfinder LAND_IMPASSABLE_FLAGS.
const FORBIDDEN: Array[String] = [
"ocean", "deep_ocean", "coast", "inland_sea", "lake",
"mountains", "volcano", "ice",
]
var out: Array[Vector2i] = []
for col: int in range(w):
for row: int in range(h):
var tile: Dictionary = grid.get_tile_dict(col, row) as Dictionary
if tile.is_empty():
continue
var biome: String = String(tile.get("biome_id", ""))
if biome.is_empty() or biome in FORBIDDEN:
continue
out.append(Vector2i(col, row))
return out
func _pick_spaced_capitals(land: Array[Vector2i], count: int) -> Array[Vector2i]:
## Greedy max-distance picker: first capital = land[0]; each subsequent
## capital = the land tile maximising minimum distance to all picks.
## Deterministic given a sorted input (we don't sort — caller order
## is the grid scan order, which is itself deterministic).
var picks: Array[Vector2i] = []
if land.is_empty() or count <= 0:
return picks
picks.append(land[0])
for _i: int in range(1, count):
var best: Vector2i = land[0]
var best_min_d: int = -1
for candidate: Vector2i in land:
var min_d: int = 1_000_000
for p: Vector2i in picks:
var dx: int = candidate.x - p.x
var dy: int = candidate.y - p.y
var d: int = absi(dx) + absi(dy)
if d < min_d:
min_d = d
if min_d > best_min_d:
best_min_d = min_d
best = candidate
picks.append(best)
return picks
func _pump() -> void:
while not _shutdown:
var line: String = OS.read_string_from_stdin(4096)