diff --git a/src/game/engine/scenes/tests/iter_7g_real_mapgen_proof.gd b/src/game/engine/scenes/tests/iter_7g_real_mapgen_proof.gd new file mode 100644 index 00000000..d888ec01 --- /dev/null +++ b/src/game/engine/scenes/tests/iter_7g_real_mapgen_proof.gd @@ -0,0 +1,341 @@ +extends Node2D +## Iter 7g — Real-mapgen + GdTurnProcessor proof scene. +## +## Where iter 7e used a hand-synthesized 24×24 grid with a fixed lair +## pattern, this scene drives the full mc_mapgen pipeline end-to-end: +## +## 1. Instantiate GdMapGenerator, initialize with empty params JSON, +## and call generate(seed, "duel") to produce a real 40×24 GridState +## (the smallest predefined map size — mc-mapgen does not support +## arbitrary 32×32 dimensions). The task brief asked for 32×32, but +## "duel" is the closest canonical size and still fits the 50-turn +## loop comfortably. +## 2. Hand the resulting GdGridState to a fresh GdGameState via the +## new `set_grid_from_gridstate` method (added in iter 7g to mirror +## the existing `create_grid` entry point). +## 3. Stamp 8 lairs across all tier bands (T2-T10) at deterministic +## positions, add a militarist player, and run GdTurnProcessor for +## 50 turns. +## 4. Render the final state on a Label plus a second Label containing +## an ASCII map showing terrain symbols, lair tier digits, and the +## player's city/starter-unit positions. +## 5. Capture a screenshot with the same pattern as iter_7e. +## +## This is the first proof scene where the Godot game consumes REAL +## Rust-generated terrain alongside the Rust turn processor. + +const TOTAL_TURNS: int = 50 +const MAP_SEED: int = 20260407 +const MAP_SIZE_ID: String = "duel" # 40×24 — smallest predefined mc-mapgen size +const OUTPUT_DIR: String = "user://screenshots" +const CITY_COL: int = 20 +const CITY_ROW: int = 12 + +var _state: RefCounted = null +var _processor: RefCounted = null +var _grid: RefCounted = null +var _title: Label = null +var _label: Label = null +var _map_label: Label = null +var _turn: int = 0 +var _cumulative_encounters: int = 0 +var _cumulative_deaths: int = 0 +var _cumulative_t4_t6_enc: int = 0 +var _cumulative_t4_t6_kills: int = 0 +var _cumulative_t7_t10_enc: int = 0 +var _cumulative_t7_t10_kills: int = 0 +var _lair_placements: Array = [] +var _grid_width: int = 0 +var _grid_height: int = 0 +var _captured: bool = false +var _sim_complete: bool = false + + +func _ready() -> void: + _build_ui() + + var ok: bool = _instantiate_bridge() + if not ok: + _title.text = "FAIL: GDExtension classes not registered" + _title.add_theme_color_override("font_color", Color.RED) + await get_tree().create_timer(2.0).timeout + _capture_and_quit("iter_7g_real_mapgen_fail") + return + + _generate_grid() + _attach_grid_to_state() + _seed_lairs() + _add_player() + _redraw() + + await get_tree().create_timer(0.3).timeout + _run_simulation() + _redraw() + + await get_tree().create_timer(1.5).timeout + _capture_and_quit("iter_7g_real_mapgen") + + +func _build_ui() -> void: + DisplayServer.window_set_size(Vector2i(1920, 1080)) + get_viewport().size = Vector2i(1920, 1080) + + var bg: ColorRect = ColorRect.new() + bg.color = Color(0.08, 0.08, 0.12) + bg.size = Vector2(1920, 1080) + add_child(bg) + + _title = Label.new() + _title.text = "Iter 7g — Real mapgen + GdTurnProcessor proof" + _title.position = Vector2(60, 32) + _title.add_theme_font_size_override("font_size", 38) + _title.add_theme_color_override("font_color", Color(0.9, 0.95, 1.0)) + add_child(_title) + + _label = Label.new() + _label.position = Vector2(60, 100) + _label.add_theme_font_size_override("font_size", 22) + _label.add_theme_color_override("font_color", Color(0.85, 0.85, 0.9)) + add_child(_label) + + _map_label = Label.new() + _map_label.position = Vector2(60, 560) + _map_label.add_theme_font_size_override("font_size", 16) + _map_label.add_theme_color_override("font_color", Color(0.7, 0.95, 0.7)) + add_child(_map_label) + + +func _instantiate_bridge() -> bool: + _processor = ClassDB.instantiate("GdTurnProcessor") as RefCounted + if _processor == null: + push_error("GdTurnProcessor not registered in GDExtension") + return false + + _state = ClassDB.instantiate("GdGameState") as RefCounted + if _state == null: + push_error("GdGameState not registered in GDExtension") + return false + + # Disable the advisory victory check so the 50-turn loop never trips it. + _processor.call("set_victory_city_count", 255) + _processor.call("set_max_turns", 999999) + return true + + +func _generate_grid() -> void: + var mapgen: RefCounted = ClassDB.instantiate("GdMapGenerator") as RefCounted + if mapgen == null: + push_error("GdMapGenerator not registered in GDExtension") + return + mapgen.call("initialize", "{}") + _grid = mapgen.call("generate", MAP_SEED, MAP_SIZE_ID) as RefCounted + if _grid == null: + push_error("GdMapGenerator.generate returned null") + return + _grid_width = int(_grid.call("get_width")) + _grid_height = int(_grid.call("get_height")) + + +func _attach_grid_to_state() -> void: + if _grid == null or _state == null: + return + _state.call("set_grid_from_gridstate", _grid) + + +func _seed_lairs() -> void: + # Ten lairs spanning T2-T10. Positions are deterministic and fit + # inside a 40×24 "duel" map. A T10 boss lair sits adjacent to the + # city so the 50-turn loop produces visible high-tier encounter + # pressure on the screenshot. + _lair_placements = [ + [4, 4, 2, 101], + [6, 18, 3, 102], + [30, 5, 2, 103], + [10, 9, 5, 201], + [25, 15, 4, 202], + [3, 12, 6, 203], + [21, 12, 10, 901], + [35, 20, 8, 902], + [17, 3, 9, 903], + [4, 20, 7, 904], + ] + for p: Array in _lair_placements: + var ok: bool = bool( + _state.call("stamp_lair", p[0], p[1], p[2], p[3]) + ) + if not ok: + push_warning( + "stamp_lair failed at (%d,%d) tier=%d" % [p[0], p[1], p[2]] + ) + + +func _add_player() -> void: + var pi: int = int(_state.call("add_player_militarist", CITY_COL, CITY_ROW)) + if pi != 0: + push_warning("expected player_index 0, got %d" % pi) + + +func _run_simulation() -> void: + for i: int in range(TOTAL_TURNS): + var result: Dictionary = _processor.call("step", _state) + _turn = int(result.get("turn", 0)) + _cumulative_encounters += int(result.get("encounters", 0)) + _cumulative_deaths += int(result.get("deaths", 0)) + _cumulative_t4_t6_enc += int(result.get("t4_t6_encounters", 0)) + _cumulative_t4_t6_kills += int(result.get("t4_t6_deaths", 0)) + _cumulative_t7_t10_enc += int(result.get("t7_t10_encounters", 0)) + _cumulative_t7_t10_kills += int(result.get("t7_t10_deaths", 0)) + _sim_complete = true + + +func _redraw() -> void: + var cities: int = int(_state.call("city_count", 0)) + var units: int = int(_state.call("unit_count", 0)) + var gold: int = int(_state.call("gold", 0)) + var lairs: int = int(_state.call("lair_count")) + + var t4_kr: float = 0.0 + if _cumulative_t4_t6_enc > 0: + t4_kr = ( + 100.0 * float(_cumulative_t4_t6_kills) / float(_cumulative_t4_t6_enc) + ) + var t7_kr: float = 0.0 + if _cumulative_t7_t10_enc > 0: + t7_kr = ( + 100.0 * float(_cumulative_t7_t10_kills) + / float(_cumulative_t7_t10_enc) + ) + + var sim_state: String = "COMPLETE" if _sim_complete else "running" + var lines: Array[String] = [ + "Bridge: GdMapGenerator → GdGridState → GdGameState → GdTurnProcessor", + "Mapgen: seed=%d size=%s (%d×%d real-terrain grid)" % [ + MAP_SEED, MAP_SIZE_ID, _grid_width, _grid_height + ], + "Player: militarist (exp=2 prod=5 wealth=2 culture=2) @ (%d,%d)" % [ + CITY_COL, CITY_ROW + ], + "Lairs: %d stamped across T2-T10" % lairs, + "", + "Turn: %d / %d (sim %s)" % [_turn, TOTAL_TURNS, sim_state], + "", + "Player 0 state:", + " cities: %d units: %d gold: %d" % [cities, units, gold], + "", + "Fauna pressure (cumulative):", + " encounters: %d" % _cumulative_encounters, + " unit deaths: %d" % _cumulative_deaths, + " T4-T6: %d / %d = %.1f%% kill rate" % [ + _cumulative_t4_t6_kills, _cumulative_t4_t6_enc, t4_kr + ], + " T7-T10: %d / %d = %.1f%% kill rate" % [ + _cumulative_t7_t10_kills, _cumulative_t7_t10_enc, t7_kr + ], + ] + _label.text = "\n".join(lines) + + _map_label.text = _render_ascii_map() + + +func _render_ascii_map() -> String: + if _grid == null or _grid_width <= 0 or _grid_height <= 0: + return "(no grid)" + + # Build lair lookup keyed on "col,row" → tier. + var lair_lookup: Dictionary = {} + for p: Array in _lair_placements: + var key: String = "%d,%d" % [int(p[0]), int(p[1])] + lair_lookup[key] = int(p[2]) + + var header: String = "Real-terrain map (#=city u=unit 1-9/A=lair tier .,~,^,*,o=biome)" + var rows: Array[String] = [header, ""] + for row: int in range(_grid_height): + var parts: Array[String] = [] + for col: int in range(_grid_width): + var key: String = "%d,%d" % [col, row] + if col == CITY_COL and row == CITY_ROW: + parts.append("#") + continue + if col > CITY_COL and col < CITY_COL + 3 and row == CITY_ROW: + # Starter units spawn here in add_player_militarist. + parts.append("u") + continue + if lair_lookup.has(key): + var tier: int = int(lair_lookup[key]) + parts.append(_tier_glyph(tier)) + continue + var tile: Dictionary = _grid.call("get_tile_dict", col, row) as Dictionary + var biome_id: String = str(tile.get("biome_id", "")) + parts.append(_biome_glyph(biome_id)) + rows.append("".join(parts)) + return "\n".join(rows) + + +func _tier_glyph(tier: int) -> String: + if tier <= 0: + return "." + if tier <= 9: + return str(tier) + return "A" # T10 renders as A + + +func _biome_glyph(biome_id: String) -> String: + var glyph: String = "." + if biome_id == "": + return glyph + if ( + biome_id.contains("ocean") + or biome_id.contains("sea") + or biome_id.contains("water") + ): + glyph = "~" + elif biome_id.contains("mountain") or biome_id.contains("hill"): + glyph = "^" + elif ( + biome_id.contains("forest") + or biome_id.contains("jungle") + or biome_id.contains("taiga") + ): + glyph = "*" + elif biome_id.contains("desert"): + glyph = ":" + elif ( + biome_id.contains("tundra") + or biome_id.contains("ice") + or biome_id.contains("snow") + ): + glyph = "o" + return glyph + + +func _capture_and_quit(shot_name: String) -> void: + if _captured: + return + _captured = true + + DirAccess.make_dir_recursive_absolute( + ProjectSettings.globalize_path(OUTPUT_DIR) + ) + + var image: Image = get_viewport().get_texture().get_image() + if image == null: + push_error("iter_7g_proof: viewport get_image returned null") + get_tree().quit(1) + return + + var timestamp: String = Time.get_datetime_string_from_system().replace( + ":", "-" + ).replace("T", "_") + var rel_path: String = "%s/%s_%s.png" % [OUTPUT_DIR, shot_name, timestamp] + var abs_path: String = ProjectSettings.globalize_path(rel_path) + var err: Error = image.save_png(abs_path) + + if err == OK: + print("SCREENSHOT_PATH:%s" % abs_path) + print("iter_7g_proof: %dx%d saved to %s" % [ + image.get_width(), image.get_height(), abs_path + ]) + else: + push_error("iter_7g_proof: save failed: %s" % error_string(err)) + + get_tree().quit() diff --git a/src/game/engine/scenes/tests/iter_7g_real_mapgen_proof.tscn b/src/game/engine/scenes/tests/iter_7g_real_mapgen_proof.tscn new file mode 100644 index 00000000..4de23e91 --- /dev/null +++ b/src/game/engine/scenes/tests/iter_7g_real_mapgen_proof.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://iter7g_real_mapgen_proof1"] + +[ext_resource type="Script" path="res://engine/scenes/tests/iter_7g_real_mapgen_proof.gd" id="1_script"] + +[node name="Iter7gRealMapgenProof" type="Node2D"] +script = ExtResource("1_script")