Skip to content

Commit 62eda98

Browse files
authored
Merge pull request #108 from tamirelazar/dev
Promote dev to master: cell-path overlays + headless-ansi (8a)
2 parents 5a2ed0d + 9b0a4e2 commit 62eda98

6 files changed

Lines changed: 567 additions & 27 deletions

File tree

src/app/mod.rs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::exploration::{Explorer, ExplorerConfig, PresetBehavior};
1212
use crate::export::GifExporter;
1313
use crate::export::WebmExporter;
1414
use crate::render::adaptive_brightness::AdaptiveBrightness;
15+
use crate::render::ansi::{render_ansi_framed, FrameGeometry};
1516
use crate::render::charset::Charset;
1617
use crate::render::dither::DitherMode;
1718
use 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+
};
2427
use 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};
2731
use crate::simulation;
2832
use 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(

src/cli.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub enum Mode {
3636
GifExport,
3737
/// Export animation to WebM.
3838
WebmExport,
39+
/// Deterministic headless ANSI golden-frame mode (mirrors the wasm
40+
/// `render_ansi_frame`, byte-for-byte, from fresh isolated state).
41+
HeadlessAnsi,
3942
}
4043

4144
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -671,6 +674,40 @@ pub struct Args {
671674
/// Directory to save captured frames.
672675
pub frame_dir: String,
673676

677+
#[arg(
678+
long = "headless-ansi",
679+
help = "Print the exact ANSI frame the wasm's render_ansi_frame produces and exit (deterministic golden mode)"
680+
)]
681+
/// Deterministic headless ANSI golden-frame mode (mirrors the wasm `render_ansi_frame`).
682+
pub headless_ansi: bool,
683+
684+
#[arg(
685+
long = "cols",
686+
value_name = "INT",
687+
default_value_t = 80,
688+
help = "Terminal columns for --headless-ansi"
689+
)]
690+
/// Terminal column count used by `--headless-ansi`.
691+
pub headless_cols: usize,
692+
693+
#[arg(
694+
long = "rows",
695+
value_name = "INT",
696+
default_value_t = 24,
697+
help = "Terminal rows for --headless-ansi"
698+
)]
699+
/// Terminal row count used by `--headless-ansi`.
700+
pub headless_rows: usize,
701+
702+
#[arg(
703+
long = "steps",
704+
value_name = "INT",
705+
default_value_t = 300,
706+
help = "Simulation steps to run for --headless-ansi"
707+
)]
708+
/// Simulation step count used by `--headless-ansi`.
709+
pub headless_steps: usize,
710+
674711
#[arg(
675712
short = 's',
676713
long = "seed",
@@ -1813,7 +1850,9 @@ pub struct Args {
18131850
impl Args {
18141851
/// Determines the operational mode based on flags.
18151852
pub fn mode(&self) -> Mode {
1816-
if self.screensaver {
1853+
if self.headless_ansi {
1854+
Mode::HeadlessAnsi
1855+
} else if self.screensaver {
18171856
Mode::Screensaver
18181857
} else if self.live {
18191858
Mode::Live
@@ -2227,6 +2266,10 @@ impl Default for Args {
22272266
explore: false,
22282267
explore_behavior: None,
22292268
explore_iterations: 100,
2269+
headless_ansi: false,
2270+
headless_cols: 80,
2271+
headless_rows: 24,
2272+
headless_steps: 300,
22302273
seed: None,
22312274
population: Some(population::DEFAULT_POPULATION),
22322275
sensor_angle: Some(agent_consts::DEFAULT_SENSOR_ANGLE),

0 commit comments

Comments
 (0)