@@ -12,6 +12,7 @@ use crate::exploration::{Explorer, ExplorerConfig, PresetBehavior};
1212use crate :: export:: GifExporter ;
1313use crate :: export:: WebmExporter ;
1414use crate :: render:: adaptive_brightness:: AdaptiveBrightness ;
15+ use crate :: render:: ansi:: { render_ansi_framed, FrameGeometry } ;
1516use crate :: render:: charset:: Charset ;
1617use crate :: render:: dither:: DitherMode ;
1718use crate :: render:: downsample:: { downsample, DownsampledFrame } ;
@@ -20,10 +21,13 @@ use crate::render::overlay::{
2021 ConfigBrowserOverlay , ConfigSaveOverlay , DashboardOverlay , KeyboardHintsOverlay , PauseOverlay ,
2122 PresetComparisonOverlay , RenderedOverlay ,
2223} ;
23- use crate :: render:: palette:: { hex_to_rgb, palette_accent_color, RgbColor } ;
24+ use crate :: render:: palette:: {
25+ hex_to_rgb, palette_accent_color, IntensityMapping , Palette , RgbColor ,
26+ } ;
2427use crate :: render:: palette_editor:: {
2528 EditorComponent , EditorMode , PaletteEditorOverlay , PaletteEditorState ,
2629} ;
30+ use crate :: render:: window:: { FRAME_RING_COLS , FRAME_RING_ROWS , GRID_COLOR , GRID_OPACITY } ;
2731use crate :: simulation;
2832use crate :: simulation:: config:: {
2933 Attractor , DiffusionKernel , InitMode , Preset , SimConfig , TerrainType ,
@@ -624,6 +628,22 @@ pub fn run() -> io::Result<()> {
624628 return Ok ( ( ) ) ;
625629 }
626630
631+ if args. mode ( ) == Mode :: HeadlessAnsi {
632+ let seed = args. seed . unwrap_or ( 1 ) ;
633+ let agents = args. population . unwrap_or ( 50_000 ) ;
634+ print ! (
635+ "{}" ,
636+ headless_ansi_frame(
637+ args. headless_cols,
638+ args. headless_rows,
639+ args. headless_steps,
640+ seed,
641+ agents,
642+ )
643+ ) ;
644+ return Ok ( ( ) ) ;
645+ }
646+
627647 args. validate ( )
628648 . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: InvalidInput , e) ) ?;
629649
@@ -677,6 +697,72 @@ pub fn run() -> io::Result<()> {
677697 Ok ( ( ) )
678698}
679699
700+ /// Renders the exact ANSI frame the vendored wasm's `render_ansi_frame`
701+ /// produces for a `cols`×`rows` terminal, from fresh isolated state.
702+ ///
703+ /// Mirrors the `/info` route identity byte-for-byte: a 400×200 `Simulation`
704+ /// seeded with `InitMode::Random`, `Palette::Warm`, bare
705+ /// `AdaptiveBrightness::new(100, true)` gain (no brightness divisor), and the
706+ /// same grid/accent parameters — so `--headless-ansi` output is a golden
707+ /// frame the wasm reproduces byte-identically when configured to the same
708+ /// identity (Warm palette, brightness 1.0).
709+ pub fn headless_ansi_frame (
710+ cols : usize ,
711+ rows : usize ,
712+ steps : usize ,
713+ seed : u64 ,
714+ agents : usize ,
715+ ) -> String {
716+ let ( sim_w, sim_h) = ( 400usize , 200usize ) ;
717+ let mut config = SimConfig :: default ( ) ;
718+ if let Some ( species) = config. species_configs . first_mut ( ) {
719+ species. count = agents;
720+ }
721+ let mut sim = Simulation :: new ( sim_w, sim_h, config, seed, InitMode :: Random , 0 ) ;
722+
723+ let geom = FrameGeometry {
724+ cols,
725+ rows,
726+ ring_cols : FRAME_RING_COLS ,
727+ ring_rows : FRAME_RING_ROWS ,
728+ } ;
729+ let ( iw, ih) = geom. interior ( ) ;
730+
731+ let mut adaptive = AdaptiveBrightness :: new ( 100 , true ) ;
732+
733+ let mut grid = GridRenderer :: new ( GridStyle :: Cross , 5 , GRID_COLOR , GRID_OPACITY , false ) ;
734+ grid. initialize ( iw, ih) ;
735+
736+ let mut frame = DownsampledFrame :: new ( iw, ih) ;
737+ let mut trail: Vec < f32 > = Vec :: new ( ) ;
738+
739+ let mapping = IntensityMapping :: logarithmic ( 10.0 ) ;
740+ let accent = palette_accent_color ( & Palette :: Warm , false , false , 0.0 , Some ( & mapping) ) ;
741+
742+ let mut out = String :: new ( ) ;
743+ for _ in 1 ..=steps {
744+ sim. update ( 1.0 ) ;
745+ sim. trail_map_blended ( & mut trail) ;
746+ downsample ( & trail, sim_w, sim_h, iw, ih, & mut frame) ;
747+
748+ adaptive. update ( frame. cells ( ) ) ;
749+ let gain = adaptive. get_max_brightness ( ) ;
750+
751+ out = render_ansi_framed (
752+ frame. cells ( ) ,
753+ & geom,
754+ Palette :: Warm ,
755+ Charset :: Ascii ,
756+ gain,
757+ Some ( & grid) ,
758+ GRID_COLOR ,
759+ GRID_OPACITY ,
760+ Some ( accent) ,
761+ ) ;
762+ }
763+ out
764+ }
765+
680766/// Executes the "Print" mode.
681767///
682768/// Advances the simulation one step and writes a single frame to stdout,
@@ -1484,6 +1570,16 @@ mod tests {
14841570 use crate :: simulation:: config:: WindowFrame ;
14851571 use crate :: terminal:: control:: MouseInteractionMode ;
14861572
1573+ #[ test]
1574+ fn headless_ansi_is_deterministic_and_framed ( ) {
1575+ let a = crate :: app:: headless_ansi_frame ( 80 , 24 , 50 , 1 , 50_000 ) ;
1576+ let b = crate :: app:: headless_ansi_frame ( 80 , 24 , 50 , 1 , 50_000 ) ;
1577+ assert_eq ! ( a, b, "fresh state + fixed steps ⇒ identical" ) ;
1578+ assert ! ( a. contains( '\u{2588}' ) , "glow ring present" ) ;
1579+ let c = crate :: app:: headless_ansi_frame ( 80 , 24 , 51 , 1 , 50_000 ) ;
1580+ assert_ne ! ( a, c, "different step count ⇒ different frame" ) ;
1581+ }
1582+
14871583 #[ test]
14881584 fn sync_renderer_caches_pushes_charset_and_window_frame ( ) {
14891585 let mut rs = RuntimeState :: new (
0 commit comments