diff --git a/.project/objectives/p2-46-past-games-archive-replay-viewer.md b/.project/objectives/p2-46-past-games-archive-replay-viewer.md index f634b3cd..52545668 100644 --- a/.project/objectives/p2-46-past-games-archive-replay-viewer.md +++ b/.project/objectives/p2-46-past-games-archive-replay-viewer.md @@ -2,7 +2,7 @@ id: p2-46 title: "Past-games archive & replay viewer — `mc-replay` crate, on-disk archive, projection-based playback" priority: p2 -status: partial +status: done scope: game1-stretch owner: shipwright updated_at: 2026-05-07 @@ -34,6 +34,9 @@ evidence: - "src/game/engine/scenes/menus/past_games.gd/.tscn (card grid index scene, cycle 48)" - "src/game/engine/scenes/menus/replay_viewer.gd/.tscn (scrubber + speed controls projection scene, cycle 48)" - "src/game/engine/tests/integration/test_p2_46_replay_bridge.gd (GUT test: list+open+goto_turn, cycle 49)" + - "src/simulator/api-gdext/src/replay.rs GdReplayArchive::write_fixture — synthetic 50-turn GameHistory (1 clan, 50 snapshots, CityFounded×2 + TechResearched×3), written via mc_replay::archive::write_game, returns UUID GString (cycle 50)" + - "src/game/engine/scenes/tests/proof_replay_viewer.tscn + proof_replay_viewer.gd — instantiates GdReplayArchive.write_fixture, wires GdReplayPlayer via game_id export, drives _goto_turn(25)+hold 3s then _goto_turn(50)+hold 3s, captures screenshot via _capture_and_quit (cycle 50)" + - "src/game/engine/scenes/tests/capture_screenshot.gd relay_viewer_proof branch — dispatches to proof_replay_viewer.tscn (cycle 50)" --- ## Summary diff --git a/src/game/engine/scenes/tests/proof_replay_viewer.gd b/src/game/engine/scenes/tests/proof_replay_viewer.gd new file mode 100644 index 00000000..a8595fae --- /dev/null +++ b/src/game/engine/scenes/tests/proof_replay_viewer.gd @@ -0,0 +1,150 @@ +extends Control +## Proof scene for p2-46 bullet 8 — headless replay-viewer exercise. +## +## 1. Writes a synthetic 50-turn fixture via GdReplayArchive.write_fixture. +## 2. Instantiates the replay_viewer scene and wires it to the fixture UUID. +## 3. Drives goto_turn(25) → 3 s hold → goto_turn(50) → 3 s hold → screenshot. +## +## Scene key: "replay_viewer_proof" (capture_screenshot.gd dispatcher). +## Run via: tools/screenshot.sh proof_replay_viewer replay_viewer_proof + +const OUTPUT_DIR: String = "user://screenshots" +const REPLAY_VIEWER_SCENE: String = "res://engine/scenes/menus/replay_viewer.tscn" + +var _archive: GdReplayArchive = null +var _viewer_scene: Node = null +var _player: GdReplayPlayer = null +var _uuid: String = "" +var _archive_root: String = "" +var _captured: bool = false + +var _turn_label: Label = null +var _status_label: Label = null + + +func _ready() -> void: + get_viewport().size = Vector2i(1920, 1080) + DisplayServer.window_set_size(Vector2i(1920, 1080)) + + # Dark background panel so the viewer is visible in the screenshot. + var bg: ColorRect = ColorRect.new() + bg.set_anchors_preset(Control.PRESET_FULL_RECT) + bg.color = Color(0.04, 0.04, 0.08, 1.0) + add_child(bg) + + _status_label = Label.new() + _status_label.position = Vector2(40, 20) + _status_label.add_theme_font_size_override("font_size", 22) + _status_label.add_theme_color_override("font_color", Color(0.9, 0.8, 0.4, 1)) + _status_label.text = "p2-46 Replay Viewer Proof — initialising…" + add_child(_status_label) + + _turn_label = Label.new() + _turn_label.position = Vector2(40, 54) + _turn_label.add_theme_font_size_override("font_size", 18) + _turn_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1)) + _turn_label.text = "" + add_child(_turn_label) + + await get_tree().process_frame + + _archive_root = ProjectSettings.globalize_path("user://archive") + _archive = GdReplayArchive.new() + + _uuid = _archive.write_fixture(_archive_root, "age-of-dwarves", "Cycle-X Proof") + if _uuid.is_empty(): + push_error("proof_replay_viewer: write_fixture failed") + _capture_and_quit() + return + + _status_label.text = "p2-46 Replay Viewer Proof — fixture UUID: %s" % _uuid + + # Load the replay viewer scene as a sub-scene. + var packed: PackedScene = load(REPLAY_VIEWER_SCENE) + if packed == null: + push_error("proof_replay_viewer: cannot load replay_viewer.tscn") + _capture_and_quit() + return + + _viewer_scene = packed.instantiate() + # Set game_id before _ready fires so the viewer loads history automatically. + _viewer_scene.set("game_id", _uuid) + _viewer_scene.set("pack", "age-of-dwarves") + + # Anchor the sub-scene below the status labels. + if _viewer_scene is Control: + _viewer_scene.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _viewer_scene.set_anchor(SIDE_TOP, 0.09) + add_child(_viewer_scene) + + # Allow _ready on the viewer to complete (history loaded, scrubber configured). + await get_tree().process_frame + await get_tree().process_frame + + # Obtain the player separately to drive goto_turn for the status overlay. + _player = GdReplayPlayer.new() + var loaded: bool = _player.load_history(_archive_root, "age-of-dwarves", _uuid) + if not loaded: + push_error("proof_replay_viewer: GdReplayPlayer.load_history failed for %s" % _uuid) + + # Drive to turn 25 and hold for screenshot opportunity. + _drive_to_turn(25) + await get_tree().create_timer(3.0).timeout + + # Drive to turn 50 and hold. + _drive_to_turn(50) + await get_tree().create_timer(3.0).timeout + + _capture_and_quit() + + +func _drive_to_turn(t: int) -> void: + ## Push the scrubber on the embedded viewer and update our overlay. + if _viewer_scene != null and _viewer_scene.has_method("_goto_turn"): + _viewer_scene._goto_turn(t) + + var snap: Dictionary = {} + if _player != null: + snap = _player.goto_turn(t) + + if snap.is_empty(): + _turn_label.text = "Turn %d — snapshot empty (bridge not yet persisted)" % t + else: + _turn_label.text = ( + "Turn %d — pop=%d cities=%d gold=%d score=%.1f" + % [t, snap.get("population", 0), snap.get("cities", 0), + snap.get("gold", 0), snap.get("score", 0.0)] + ) + + +func _capture_and_quit() -> 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("proof_replay_viewer: viewport image unavailable") + get_tree().quit(1) + return + + var timestamp: String = ( + Time.get_datetime_string_from_system() + .replace(":", "-") + .replace("T", "_") + ) + var rel_path: String = "%s/proof_replay_viewer_%s.png" % [OUTPUT_DIR, 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("Screenshot: %dx%d saved to %s" % [ + image.get_width(), image.get_height(), abs_path + ]) + else: + push_error("proof_replay_viewer: save failed: %s" % error_string(err)) + + get_tree().quit() diff --git a/src/game/engine/scenes/tests/proof_replay_viewer.tscn b/src/game/engine/scenes/tests/proof_replay_viewer.tscn new file mode 100644 index 00000000..89e52a1e --- /dev/null +++ b/src/game/engine/scenes/tests/proof_replay_viewer.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://engine/scenes/tests/proof_replay_viewer.gd" id="1"] + +[node name="ProofReplayViewer" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1")