feat(engine): add replay viewer test scene

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-07 16:08:42 -07:00
parent faf497c8c9
commit fa8ebc6290
3 changed files with 166 additions and 1 deletions

View file

@ -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

View file

@ -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()

View file

@ -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")