Skip to content

Commit 2e1a429

Browse files
authored
feat: constellations preset (seeded asterisms, template-held) (#59)
* feat: add constellation asterism data and seeded pick * feat: fit constellation figures to grid with aspect correction * feat: rasterize anti-aliased constellation template * feat: seed agents along constellation stars and edges * fix: document and assert seed_agents empty-Vec precondition * feat: wire InitMode::Constellation seeding and trail pre-seed Add InitMode::Constellation variant, SimConfig::constellation_restamp_floor field, Simulation::constellation_template, build-once layout in new()/reset(), trail pre-seed via max-combine, and extend init_species with constellation_layout param + dispatch arm. Add non-exhaustive arms in runner.rs and explorer.rs. * feat: static constellation re-stamp via post-decay max * test: strengthen constellation drift assertion to verify no re-stamp * feat: add ConstellationStatic preset and retune Constellation for atlas init * feat: render both constellation presets as Braille atlas linework * fix: make Constellation reset deterministic, drop thread_rng re-roll `effective_init_mode` was calling `InitMode::random(&mut thread_rng())` for Constellation, making every auto-reset and manual restart choose a different init layout. Now that the constellation figure is selected by the seeded sim RNG inside `Simulation::new`, the reset path is fully deterministic. The function becomes a pass-through and the `preset` argument is dropped at the function and all three call sites. Stale doc comments updated accordingly. * test: register ConstellationStatic and regenerate preset goldens Add "constellationstatic" to the PRESETS array in preset_config_snapshot.rs and regenerate tests/golden/preset_configs.txt. The constellation block now reflects init_mode=Constellation (changed from Random), and a new constellationstatic block appears with restamp_floor-discriminated scalars. Fix two clippy field_reassign_with_default errors in constellation test helpers in src/simulation/mod.rs (use struct-init syntax). Full gate: 1017 lib + 56 visual_regression + 19 doc tests, all green. * fix: make ConstellationStatic never-reset behavior explicit * feat: make constellation template-led — scale to brightness, weight stars, retune presets Template values (0..1) are now multiplied by max_brightness at every stamp site (new(), reset(), update()) so the pre-seeded figure renders at full trail-brightness range instead of ~3% white-point. Star sigma tightened to 2.2px; edge peak/sigma reduced to 0.45/0.55 for hairline linework. ConstellationStatic: restamp_floor 1.0, decay 0.92, deposit 0.6, count 2500. Constellation drift: decay 0.95, deposit 1.5, count 5000, floor stays 0.0. Test rewritten to assert held > 1.5 (proves scaling happened) and that drift decays strictly below the static floor after 20 frames. * feat: constellation v2 — points render, lin/log split, low white-point, richer canvas-filling figures - Render: both Constellation + ConstellationStatic switch Braille→Points charset, add IntensityMapping::linear_log_split(10.0), explicitly pin auto_normalize=false - ConstellationStatic sim retune: max_brightness 30→10, deposit_amount 0.6→0.3 (top-level and species[0]), count 2500→2000 - fit_to_grid margin 10%→5% so figures use more of the canvas - Replace 6 figures (drop Cassiopeia/Crux): Orion(10s/11e) Ursa Major(10s/10e) Scorpius(10s/9e) Cygnus(7s/6e) Leo(9s/9e) Gemini(11s/10e) - Update test: both_constellations_render_braille_cosmic → both_constellations_render_points_cosmic (asserts Points/Cosmic/false/lin-log-split) - Regenerate preset_configs.txt golden (constellationstatic block only) * feat: unify into single 'constellations' preset (static), drop drift variant
1 parent 063effe commit 2e1a429

13 files changed

Lines changed: 759 additions & 67 deletions

src/app/mod.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,10 @@ pub(crate) fn apply_overrides(
338338
let init = profile.sim.preferred_init_mode.unwrap_or(InitMode::Food);
339339
let seed = profile.seed.unwrap_or_else(fresh_seed);
340340
rs.original_seed = seed;
341-
// Baseline stays the preset's preferred mode (non-mutating w.r.t. the
342-
// reroll) so dirty-projection sees no permanent edit.
341+
// Baseline stays the preset's preferred mode (non-mutating w.r.t. any
342+
// prior state) so dirty-projection sees no permanent edit.
343343
rs.original_init_mode = init;
344-
// Constellation re-rolls a fresh layout on this (re-)seed. The preset being
345-
// applied comes from the overrides; fall back to the live preset for config
346-
// loads / resets that don't pin a preset.
347-
let effective_preset = ov.preset.unwrap_or(rs.current_preset);
348-
let effective_init = crate::app::runner::effective_init_mode(effective_preset, init);
344+
let effective_init = crate::app::runner::effective_init_mode(init);
349345
sim.reset(seed, effective_init);
350346
}
351347

src/app/runner.rs

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,11 @@ fn update_food_persistence(sim: &mut Simulation, runtime_state: &mut RuntimeStat
8484
}
8585
}
8686

87-
/// The init mode to actually seed with. Constellation re-rolls a fresh random
88-
/// mode each reset; every other preset uses its stable baseline. Does NOT mutate
89-
/// config or `original_init_mode`, so dirty-projection stays clean.
90-
pub(crate) fn effective_init_mode(preset: Preset, base: InitMode) -> InitMode {
91-
if matches!(preset, Preset::Constellation) {
92-
InitMode::random(&mut rand::thread_rng())
93-
} else {
94-
base
95-
}
87+
/// The init mode to actually seed with. The constellation figure is chosen by
88+
/// the seeded simulation RNG inside `Simulation::new`, so reset stays
89+
/// deterministic and every preset uses its stable baseline init mode.
90+
pub(crate) fn effective_init_mode(base: InitMode) -> InitMode {
91+
base
9692
}
9793

9894
/// Checks if simulation should auto-reset based on entropy collapse.
@@ -132,11 +128,7 @@ fn check_auto_reset(
132128

133129
// Use the live original init mode (the apply seam updates it on restart),
134130
// NOT the startup `init_mode` local which is stale after a config load.
135-
// Constellation re-rolls a fresh layout each reset (non-mutating).
136-
let init_mode = effective_init_mode(
137-
runtime_state.current_preset,
138-
runtime_state.original_init_mode,
139-
);
131+
let init_mode = effective_init_mode(runtime_state.original_init_mode);
140132
sim.reset(new_seed, init_mode);
141133
runtime_state.reset_collapse_counter();
142134
runtime_state.reset_warmup();
@@ -1049,6 +1041,7 @@ pub fn run_simulation(
10491041
InitMode::RandomClusters => "Clusters",
10501042
InitMode::Food => "Food",
10511043
InitMode::Petri => "Petri",
1044+
InitMode::Constellation => "Constellation",
10521045
};
10531046

10541047
let color_mode_name = match color_mode {
@@ -1759,12 +1752,7 @@ pub fn run_simulation(
17591752
}
17601753
}
17611754
ControlAction::Restart => {
1762-
// Constellation re-rolls a fresh layout each reset
1763-
// (non-mutating: original_init_mode is untouched).
1764-
let init_mode = effective_init_mode(
1765-
runtime_state.current_preset,
1766-
runtime_state.original_init_mode,
1767-
);
1755+
let init_mode = effective_init_mode(runtime_state.original_init_mode);
17681756
sim.reset(runtime_state.original_seed, init_mode);
17691757
}
17701758
ControlAction::QuickKey(c) => match resolve_bind(c, &user_binds) {
@@ -2894,6 +2882,16 @@ mod tests {
28942882
"CLI override retains sensor_angle"
28952883
);
28962884
}
2885+
2886+
#[test]
2887+
fn constellation_reset_is_now_stable() {
2888+
use crate::simulation::config::InitMode;
2889+
// Constellation must NOT re-roll a random init mode anymore.
2890+
assert_eq!(
2891+
effective_init_mode(InitMode::Constellation),
2892+
InitMode::Constellation
2893+
);
2894+
}
28972895
}
28982896

28992897
#[cfg(test)]

src/config_defaults.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,9 @@ pub mod color_mode {
717717
pub const DEFAULT_MODE: &str = "true";
718718
}
719719

720+
/// Default constellation re-stamp floor (0.0 = off; Static presets raise it).
721+
pub const DEFAULT_CONSTELLATION_RESTAMP_FLOOR: f32 = 0.0;
722+
720723
#[cfg(test)]
721724
mod tests {
722725
use super::*;

src/exploration/explorer.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ impl ExplorationParams {
423423
InitMode::RandomClusters => "InitMode::RandomClusters",
424424
InitMode::Food => "InitMode::Food",
425425
InitMode::Petri => "InitMode::Petri",
426+
InitMode::Constellation => "InitMode::Constellation",
426427
};
427428

428429
format!(

src/preset_app_defaults.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ impl Default for PresetAppDefaults {
2525
impl From<Preset> for PresetAppDefaults {
2626
fn from(preset: Preset) -> Self {
2727
match preset {
28-
// Constellation re-rolls a fresh layout on collapse. The entropy
29-
// detector is too eager for it (low brightness-value diversity in
30-
// coherent rotating/blob patterns reads as "dead" while they are
31-
// still alive), so default it off and rely on stagnation alone.
28+
// Constellation holds its figure indefinitely via the per-frame
29+
// template re-stamp, so it must NOT auto-reset — a reset would drop
30+
// the held figure. Entropy threshold is moot while auto_reset is
31+
// false; set 0.0 to keep the detector off explicitly.
3232
Preset::Constellation => Self {
33-
auto_reset: true,
33+
auto_reset: false,
3434
entropy_threshold: 0.0,
3535
},
3636
_ => Self::default(),
@@ -49,8 +49,9 @@ mod tests {
4949
);
5050
}
5151
#[test]
52-
fn constellation_opts_in() {
53-
assert!(PresetAppDefaults::from(Preset::Constellation).auto_reset);
52+
fn constellation_opts_out_of_auto_reset() {
53+
// Constellation holds its figure via template re-stamp — must NOT auto-reset.
54+
assert!(!PresetAppDefaults::from(Preset::Constellation).auto_reset);
5455
assert_eq!(
5556
PresetAppDefaults::from(Preset::Network).auto_reset,
5657
crate::config_defaults::auto_reset::DEFAULT_AUTO_RESET

src/preset_sim_defaults.rs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ pub(crate) struct PresetSimDefaults {
4747
pub separate_species_trails: bool,
4848
pub sampling_mode: SamplingMode,
4949
pub respawn_config: RespawnConfig,
50+
/// Constellation atlas re-stamp floor. 0.0 = drift (no re-stamp);
51+
/// > 0.0 = continuous self-healing template re-stamp (static hold).
52+
pub constellation_restamp_floor: f32,
5053
}
5154

5255
impl Default for PresetSimDefaults {
@@ -80,6 +83,8 @@ impl Default for PresetSimDefaults {
8083
separate_species_trails: false,
8184
sampling_mode: SamplingMode::Nearest,
8285
respawn_config: RespawnConfig::default(),
86+
constellation_restamp_floor:
87+
crate::config_defaults::DEFAULT_CONSTELLATION_RESTAMP_FLOOR,
8388
}
8489
}
8590
}
@@ -114,6 +119,7 @@ impl PresetSimDefaults {
114119
config.separate_species_trails = self.separate_species_trails;
115120
config.sampling_mode = self.sampling_mode;
116121
config.respawn_config = self.respawn_config;
122+
config.constellation_restamp_floor = self.constellation_restamp_floor;
117123
}
118124
}
119125

@@ -646,24 +652,25 @@ impl From<Preset> for PresetSimDefaults {
646652
}],
647653
..Self::default()
648654
},
649-
// Sparse star-map scatter (config.rs:806-826)
655+
// Constellation: continuous template re-stamp holds the figure crisp
650656
Preset::Constellation => Self {
651657
sensor_angle: 45.0,
652658
sensor_distance: 12.0,
653-
rotation_angle: 25.0,
654-
step_size: 0.8,
655-
decay_factor: 0.96,
656-
deposit_amount: 4.0,
659+
rotation_angle: 18.0,
660+
step_size: 0.3,
661+
decay_factor: 0.92,
662+
deposit_amount: 0.3,
657663
diffusion_kernel: DiffusionKernel::Mean3x3,
658-
max_brightness: 30.0,
659-
preferred_init_mode: Some(InitMode::Random),
664+
max_brightness: 10.0,
665+
preferred_init_mode: Some(InitMode::Constellation),
666+
constellation_restamp_floor: 1.0,
660667
species_configs: vec![SpeciesConfig {
661668
name: "default".to_string(),
662-
count: 12_000,
669+
count: 2_000,
663670
sensor_angle: 45.0,
664-
rotation_angle: 25.0,
665-
step_size: 0.8,
666-
deposit_amount: 4.0,
671+
rotation_angle: 18.0,
672+
step_size: 0.3,
673+
deposit_amount: 0.3,
667674
..Default::default()
668675
}],
669676
..Self::default()
@@ -1020,4 +1027,20 @@ mod tests {
10201027
);
10211028
}
10221029
}
1030+
1031+
#[test]
1032+
fn constellation_is_registered_and_static() {
1033+
use crate::simulation::config::{preset_from_name, InitMode, Preset, SimConfig};
1034+
// Primary name
1035+
let p = preset_from_name("constellations").expect("preset registered");
1036+
assert_eq!(p, Preset::Constellation);
1037+
// Alias
1038+
let p2 = preset_from_name("constellation").expect("alias registered");
1039+
assert_eq!(p2, Preset::Constellation);
1040+
1041+
let cfg: SimConfig = Preset::Constellation.into();
1042+
assert!(cfg.constellation_restamp_floor > 0.0);
1043+
// preferred_init_mode is Option<InitMode>; unwrap since the arm sets Some(_)
1044+
assert_eq!(cfg.preferred_init_mode.unwrap(), InitMode::Constellation);
1045+
}
10231046
}

src/profile_overrides.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,26 +1943,26 @@ mod tests {
19431943
);
19441944
}
19451945

1946-
/// Constellation resolves `auto_reset = true` when CLI does not override it.
1946+
/// Constellation resolves `auto_reset = false` (static hold) when CLI does not override it.
19471947
#[test]
19481948
fn resolve_app_uses_preset_auto_reset_default() {
19491949
let ov = ProfileOverrides {
19501950
preset: Some(crate::simulation::config::Preset::Constellation),
19511951
..Default::default()
19521952
};
19531953
assert!(
1954-
ov.resolve_app().auto_reset,
1955-
"constellation auto_reset should be on by default"
1954+
!ov.resolve_app().auto_reset,
1955+
"constellation auto_reset should be off by default (static hold)"
19561956
);
19571957
// Explicit override wins over the preset's default.
19581958
let ov2 = ProfileOverrides {
19591959
preset: Some(crate::simulation::config::Preset::Constellation),
1960-
auto_reset: Some(false),
1960+
auto_reset: Some(true),
19611961
..Default::default()
19621962
};
19631963
assert!(
1964-
!ov2.resolve_app().auto_reset,
1965-
"explicit Some(false) must override the preset default"
1964+
ov2.resolve_app().auto_reset,
1965+
"explicit Some(true) must override the preset default"
19661966
);
19671967
}
19681968

src/render_art_defaults.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,12 @@ impl From<Preset> for RenderArtDefaults {
208208
afterglow: 0.1,
209209
..Self::default()
210210
},
211-
// Points charset: sparse particle star-map.
211+
// Atlas linework: Points charset for crisp star dots, lin/log split mapping.
212212
Preset::Constellation => Self {
213213
charset: Some(Charset::Points),
214214
palette: Some(Palette::Cosmic),
215+
intensity_mapping: IntensityMapping::linear_log_split(10.0),
216+
auto_normalize: Some(false),
215217
..Self::default()
216218
},
217219
// Quantize mapping + Wrap palette cycling: posterized bands.
@@ -489,6 +491,18 @@ mod tests {
489491
assert_eq!(RenderArtDefaults::from(Preset::Gossamer).afterglow, 0.2);
490492
}
491493

494+
#[test]
495+
fn constellation_renders_points_cosmic() {
496+
let d = RenderArtDefaults::from(Preset::Constellation);
497+
assert_eq!(d.charset, Some(Charset::Points));
498+
assert_eq!(d.palette, Some(Palette::Cosmic));
499+
assert_eq!(d.auto_normalize, Some(false));
500+
assert_eq!(
501+
d.intensity_mapping,
502+
IntensityMapping::linear_log_split(10.0)
503+
);
504+
}
505+
492506
#[test]
493507
fn vinescii_is_ascii_vines() {
494508
use crate::preset_sim_defaults::PresetSimDefaults;

src/simulation/config.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub enum Preset {
106106
Etching,
107107
/// Color that shifts with motion direction (temporal Hue mode).
108108
Drift,
109-
/// Sparse star-map scatter (Points charset).
109+
/// Constellation: crisp star-map held via continuous template re-stamp (Points charset).
110110
Constellation,
111111
/// Posterized color bands (Quantize mapping + Wrap palette cycles).
112112
Mosaic,
@@ -274,8 +274,8 @@ pub const PRESETS: &[PresetSpec] = &[
274274
},
275275
PresetSpec {
276276
preset: Preset::Constellation,
277-
name: "Constellation",
278-
aliases: &[],
277+
name: "constellations",
278+
aliases: &["constellation", "atlas"],
279279
quick_key: Some('2'),
280280
},
281281
PresetSpec {
@@ -424,11 +424,13 @@ pub enum InitMode {
424424
Food,
425425
/// Agents distributed in a Gaussian blob at the center (Petri dish style).
426426
Petri,
427+
/// Agents seeded as a real star constellation (stars + asterism edges).
428+
Constellation,
427429
}
428430

429431
impl InitMode {
430-
/// Uniformly pick any init mode. Used by presets (e.g. Constellation) that
431-
/// re-roll their starting layout on each reset.
432+
/// Uniformly pick any init mode for non-structural presets. Named structural
433+
/// modes such as `Constellation` are excluded and keep a stable layout.
432434
///
433435
/// `ALL` is hand-maintained; the exhaustive match below is a compile-time
434436
/// guard — adding an `InitMode` variant fails to compile here until it is
@@ -459,7 +461,8 @@ impl InitMode {
459461
| InitMode::Spiral
460462
| InitMode::RandomClusters
461463
| InitMode::Food
462-
| InitMode::Petri => {}
464+
| InitMode::Petri
465+
| InitMode::Constellation => {}
463466
}
464467
}
465468
ALL[rng.gen_range(0..ALL.len())]
@@ -1395,6 +1398,10 @@ pub struct SimConfig {
13951398
pub respawn_config: RespawnConfig,
13961399
/// Trail sampling method (nearest or bilinear).
13971400
pub sampling_mode: SamplingMode,
1401+
/// Constellation atlas re-stamp strength, applied each frame after
1402+
/// diffusion/decay. 0.0 = no re-stamp (drift); > 0.0 = self-healing
1403+
/// template source (static hold).
1404+
pub constellation_restamp_floor: f32,
13981405
}
13991406

14001407
impl SimConfig {
@@ -1520,6 +1527,8 @@ impl Default for SimConfig {
15201527
},
15211528
respawn_config: RespawnConfig::default(),
15221529
sampling_mode: SamplingMode::Nearest,
1530+
constellation_restamp_floor:
1531+
crate::config_defaults::DEFAULT_CONSTELLATION_RESTAMP_FLOOR,
15231532
}
15241533
}
15251534
}

0 commit comments

Comments
 (0)