test(mapgen): ✅ Add test scene for real-world map generation validation with GDScript logic and TSCN setup
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
d035d67220
commit
16a4ab16fb
2 changed files with 347 additions and 0 deletions
341
src/game/engine/scenes/tests/iter_7g_real_mapgen_proof.gd
Normal file
341
src/game/engine/scenes/tests/iter_7g_real_mapgen_proof.gd
Normal file
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
Loading…
Add table
Reference in a new issue