From df9f5c362c8a77bd994024282ff10f426b606129 Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 11 May 2026 00:56:40 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20enhance=20player=20boot=20with=20real=20map=20gener?= =?UTF-8?q?ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../scenes/headless/claude_player_main.gd | 92 ++++++++++++++++--- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/src/game/engine/scenes/headless/claude_player_main.gd b/src/game/engine/scenes/headless/claude_player_main.gd index 3e182ea1..6d37a317 100644 --- a/src/game/engine/scenes/headless/claude_player_main.gd +++ b/src/game/engine/scenes/headless/claude_player_main.gd @@ -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)