Skip to main content

rlevo_core/render/
ascii.rs

1//! Optional plain-text rendering surface for environments.
2//!
3//! [`AsciiRenderable`] is an **optional debug helper**, not a library
4//! invariant (ADR-0013). The two visualisation products do not depend on it:
5//! the live TUI is metrics-only and renders no env, and the post-run report
6//! renders env playback from the structured `FamilyPayload` carried in each
7//! `EpisodeRecord` frame. An env implements this trait only when a quick
8//! grep-friendly text dump is useful — for logs, snapshot tests, or a
9//! user-built env that wants the report's legacy `<pre>` fallback without
10//! writing a structured payload adapter.
11//!
12//! The trait lives in `rlevo-core` (rather than `rlevo-environments`) so that
13//! `rlevo-benchmarks` can still bound on it without a circular package dep.
14//! It is not a supertrait of `Environment`.
15
16use super::{Renderer, StyledFrame};
17
18/// An environment that can render itself as an ASCII string.
19///
20/// Optional (ADR-0013): implement it only for envs that want a text dump.
21/// The [`AsciiRenderer`] delegates to [`render_ascii`](Self::render_ascii)
22/// and returns the `String` as its `Frame`.
23///
24/// # Two projections
25///
26/// Two methods, two consumers:
27///
28/// - [`render_ascii`](Self::render_ascii) returns a plain `String` for logs,
29///   snapshot tests, grep-friendly output, and the optional
30///   `FrameRecord.ascii` slot. Every implementor must provide it.
31/// - [`render_styled`](Self::render_styled) returns a
32///   [`StyledFrame`] carrying colour and modifier hints for the report's
33///   legacy `<pre>`/CSS-span fallback. The default impl wraps the plain text
34///   as a single unstyled span, so implementors that only want the plain
35///   projection compile without changes.
36///
37/// Override `render_styled` only when the text dump benefits from colour
38/// cues. Use the project palette constants in [`super::palette`] rather than
39/// raw [`super::Color`] values so that the accessibility contract
40/// (hue-redundant signalling for hazard/goal semantics) is preserved.
41pub trait AsciiRenderable {
42    /// Produce a text representation of the current environment state.
43    fn render_ascii(&self) -> String;
44
45    /// Produce a styled projection of the current environment state.
46    ///
47    /// The default implementation wraps `render_ascii` as a single unstyled
48    /// span per line — sufficient for envs that ship only the plain
49    /// projection. Envs that want colour override this method directly.
50    fn render_styled(&self) -> StyledFrame {
51        StyledFrame::unstyled(self.render_ascii())
52    }
53}
54
55/// A renderer that produces ASCII `String` frames.
56///
57/// Implements [`Renderer<E>`](super::Renderer) for any `E: AsciiRenderable`,
58/// delegating to [`AsciiRenderable::render_ascii`].  Pair with
59/// [`NullRenderer`](crate::render::NullRenderer) when rendering is
60/// disabled so the call compiles away entirely.
61///
62/// # Example
63///
64/// ```no_run
65/// use rlevo_core::render::{AsciiRenderable, AsciiRenderer, Renderer};
66///
67/// struct GridEnv { width: usize, height: usize }
68///
69/// impl AsciiRenderable for GridEnv {
70///     fn render_ascii(&self) -> String {
71///         (0..self.height)
72///             .map(|_| ".".repeat(self.width))
73///             .collect::<Vec<_>>()
74///             .join("\n")
75///     }
76/// }
77///
78/// let env = GridEnv { width: 5, height: 3 };
79/// let renderer = AsciiRenderer;
80/// let frame: String = renderer.render(&env);
81/// println!("{frame}");
82/// ```
83#[derive(Debug, Clone, Copy, Default)]
84pub struct AsciiRenderer;
85
86impl<E: AsciiRenderable> Renderer<E> for AsciiRenderer {
87    type Frame = String;
88
89    fn render(&self, env: &E) -> String {
90        env.render_ascii()
91    }
92}