feat(@projects/@magic-civilization): ✨ enhance player boot with real map generation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
cb78384088
commit
df9f5c362c
1 changed files with 77 additions and 15 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue