diff --git a/src/game/engine/scenes/tests/claude_vs_ai_render_proof.gd b/src/game/engine/scenes/tests/claude_vs_ai_render_proof.gd index 7a81c72b..aa7bd0a8 100644 --- a/src/game/engine/scenes/tests/claude_vs_ai_render_proof.gd +++ b/src/game/engine/scenes/tests/claude_vs_ai_render_proof.gd @@ -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 ─────────────────────────────────────────────────────────────────