feat(test): add capital zoom and hud overlay for render proof

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-12 12:03:10 -07:00
parent 8796e7a06d
commit 8fc97ffaed

View file

@ -281,16 +281,62 @@ func _build_renderers() -> void:
var world_per_screen: float = max(fit_x, fit_y)
var zoom_factor: float = 1.0 / max(world_per_screen, 0.001)
_camera_overview_pos = center
_camera_overview_zoom = zoom_factor
# Capital-zoom preset: 9-tile wide window centred on player 0's
# starting capital. The renderer's procedural unit circle is ~30 px
# wide which is invisible at the overview zoom (a duel-map fits
# 1920×1080 with sub-pixel unit dots); capital zoom shows units and
# city sprites unambiguously. Falls back to map centre if start_positions
# is empty.
var capital_axial: Vector2i = Vector2i.ZERO
if _game_map.start_positions.size() > 0:
capital_axial = _game_map.start_positions[0]
_camera_capital_pos = HexUtilsScript.axial_to_pixel(capital_axial)
_camera_capital_zoom = float(VIEWPORT_SIZE.x) / (9.0 * 384.0)
_camera = Camera2D.new()
_camera.name = "DemoCamera"
_camera.position = center
_camera.zoom = Vector2(zoom_factor, zoom_factor)
_camera.position = _camera_overview_pos
_camera.zoom = Vector2(_camera_overview_zoom, _camera_overview_zoom)
add_child(_camera)
# `make_current` is a no-op until the node is inside the tree, so it
# MUST come after `add_child`. The reverse order spams a Camera2D
# warning at boot and silently drops the activation.
_camera.make_current()
# HUD overlay so each PNG carries the turn number + Claude's scoreboard
# state in-frame. Without this, a reviewer can't tell two screenshots
# apart at a glance — the unit dots are sub-pixel.
_hud_layer = CanvasLayer.new()
_hud_layer.name = "HUDLayer"
add_child(_hud_layer)
_hud_label = Label.new()
_hud_label.name = "HUDLabel"
_hud_label.position = Vector2(24, 24)
_hud_label.add_theme_color_override("font_color", Color(1, 1, 1, 1))
_hud_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1))
_hud_label.add_theme_constant_override("outline_size", 6)
_hud_label.add_theme_font_size_override("font_size", 28)
_hud_layer.add_child(_hud_label)
func _set_camera_to(mode: String) -> void:
if _camera == null:
return
if mode == "capital":
_camera.position = _camera_capital_pos
_camera.zoom = Vector2(_camera_capital_zoom, _camera_capital_zoom)
else:
_camera.position = _camera_overview_pos
_camera.zoom = Vector2(_camera_overview_zoom, _camera_overview_zoom)
func _set_hud(text: String) -> void:
if _hud_label != null:
_hud_label.text = text
# ── Main loop ─────────────────────────────────────────────────────────────
@ -299,7 +345,7 @@ func _drive_game() -> void:
var initial_view: Dictionary = _request_view()
_rehydrate_view(initial_view)
await _flush_render()
_capture_screenshot(0)
await _capture_screenshot(0)
_turn_records.append({
"turn": 0,
"claude_actions": [],
@ -350,7 +396,7 @@ func _drive_game() -> void:
_turn_records.append(record)
if turn_idx % _screenshot_every == 0 or turn_idx == _max_turns:
_capture_screenshot(turn_idx)
await _capture_screenshot(turn_idx)
# GameOver check — any event with type=game_over terminates.
if _events_contain_game_over(record["end_turn_events"]):
@ -580,17 +626,47 @@ func _flush_render() -> void:
func _capture_screenshot(turn_idx: int) -> void:
var image: Image = get_viewport().get_texture().get_image()
if image == null:
push_error("viewport image null at turn %d" % turn_idx)
return
var path: String = "%s/turn-%02d.png" % [_output_dir_abs, turn_idx]
var err: Error = image.save_png(path)
if err == OK:
_captured_turns.append(turn_idx)
print("SCREENSHOT_TURN:%02d:%s" % [turn_idx, path])
# Per-turn HUD overlay carries the turn + Claude's scoreboard so each
# PNG is self-identifying. Updated before each capture pose.
var scoreboard: String = "Turn %d" % turn_idx
if _turn_records.size() > 0:
var rec: Dictionary = _turn_records[-1] as Dictionary
var scores: Array = rec.get("scores", []) as Array
if scores.size() > 0:
var s0: Dictionary = scores[0] as Dictionary
scoreboard = "Turn %d • Claude (slot %d) gold %d cities %d units %d" % [
turn_idx, _claude_slot,
int(s0.get("gold", 0)),
int(s0.get("cities", 0)),
int(s0.get("units", 0)),
]
var modes: Array[String] = []
if _zoom_mode == "both":
modes = ["overview", "capital"]
elif _zoom_mode == "capital":
modes = ["capital"]
else:
push_error("save_png failed turn=%d err=%s" % [turn_idx, error_string(err)])
modes = ["overview"]
for mode: String in modes:
_set_camera_to(mode)
_set_hud("%s [%s]" % [scoreboard, mode])
# Give the engine 2 frames to apply camera change + HUD text + redraw.
for _i: int in range(2):
await get_tree().process_frame
var image: Image = get_viewport().get_texture().get_image()
if image == null:
push_error("viewport image null at turn %d (%s)" % [turn_idx, mode])
continue
var suffix: String = "" if _zoom_mode != "both" else ("-" + mode)
var path: String = "%s/turn-%02d%s.png" % [_output_dir_abs, turn_idx, suffix]
var err: Error = image.save_png(path)
if err == OK:
_captured_turns.append(turn_idx)
print("SCREENSHOT_TURN:%02d:%s:%s" % [turn_idx, mode, path])
else:
push_error("save_png failed turn=%d mode=%s err=%s" % [turn_idx, mode, error_string(err)])
# ── Recap ─────────────────────────────────────────────────────────────────