Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ jobs:
- name: Run cargo test
run: cargo test --verbose

# Gated feature code (multi-species, audio, gui) is not compiled by the
# default build; run lib tests with all features so refactors cannot
# silently break it. Linux only: deps are installed above and the full
# GUI stack is too expensive to build on the paid runners.
- name: Run cargo test (all features, lib)
if: runner.os == 'Linux'
run: cargo test --all-features --lib

licenses:
name: License Check
needs: hygiene
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ gui = ["dep:iced", "dep:iced_term"]
terminal = ["dep:crossterm", "dep:signal-hook", "dep:memory-stats", "dep:thiserror"]
# audio (choir mode) opt-in: cargo build --features audio
audio = ["dep:cpal"]
# multi-species is experimental: cargo build --features multi-species
multi-species = []

[dependencies]
crossterm = { version = "0.27", optional = true }
Expand Down
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ Choose from 12+ built-in palettes for different moods:
|:----:|:-----:|:----:|:-----:|
| ![Heat](assets/demos/heat_small.gif) | ![Ocean](assets/demos/ocean_small.gif) | ![Neon](assets/demos/neon_small.gif) | ![Slime](assets/demos/slime_small.gif) |

### Special Effects

**Multi-species mode** - Different agents with distinct colors:

![Species Demo](assets/demos/species.gif)

---

## 🌿 Overview
Expand Down Expand Up @@ -271,7 +265,6 @@ In live or screensaver mode, you can adjust simulation parameters in real-time u
| `r` | Restart with same seed |
| `+` / `-` | Adjust time scale (0.5x - 4.0x) |
| `c` | Cycle to next palette (Shift+C for reverse) |
| `d` | Toggle dithering |
| `h` | Toggle help overlay (quick → options → none) |
| `q` / `Esc` | Quit |

Expand Down Expand Up @@ -586,6 +579,21 @@ cargo test --test visual_regression

---

## Experimental — help wanted

Features that exist but aren't ready for the default experience. Each has a
tracking issue describing current state and what's needed — contributions welcome.

| Feature | Try it | Issue |
|---|---|---|
| Multi-species simulation | `cargo install tslime --features multi-species`, then `--species 'red:20k:ff0000' --species 'blue:20k:0000ff' --species-colors` | #8 |
| Choir mode (audio) | `cargo install tslime --features audio`, then `--choir` | #9 |
| GUI mode | `cargo build --features gui` | #10 |
| WASM build | `tslime-wasm/` (standalone crate) | #11 |
| Dithering | hidden flags: `--dither-mode ordered` (and `d`/`D`/`[`/`]` keys once enabled) | #12 |

---

## 🤝 Contributing

Contributions are welcome! Areas of interest:
Expand All @@ -596,6 +604,9 @@ Contributions are welcome! Areas of interest:
- **Documentation**: Examples, tutorials, translations
- **Optimization**: SIMD, parallelization, memory improvements

All current presets are experimental — feedback on which work, which don't,
and recommendations for new ones are very welcome.

### Development Workflow

1. Fork the repository
Expand Down
26 changes: 16 additions & 10 deletions src/app/explanations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,16 @@ pub fn print_parameter_explanations() {
println!(" • clusters: Multiple random clusters");
println!(" • food: Load from image (see --food)");

println!("\n --species <SPEC>");
println!(" Define multiple species with different behaviors.");
println!(" Format: 'name:count@sensor_angle,rotation_angle,step_size,deposit:color'");
println!(
" Example: --species 'red:20k@22.5,45,1.0,5.0:ff0000,blue:30k@30,60,1.5,3.0:0000ff'"
);
println!(" Enables multi-species simulations with distinct movement patterns.");
#[cfg(feature = "multi-species")]
{
println!("\n --species <SPEC>");
println!(" Define multiple species with different behaviors.");
println!(" Format: 'name:count@sensor_angle,rotation_angle,step_size,deposit:color'");
println!(
" Example: --species 'red:20k@22.5,45,1.0,5.0:ff0000,blue:30k@30,60,1.5,3.0:0000ff'"
);
println!(" Enables multi-species simulations with distinct movement patterns.");
}

println!("\n\nENVIRONMENTAL FORCES");
println!("─────────────────────────────────────────────────────────────────────────");
Expand Down Expand Up @@ -167,7 +170,7 @@ pub fn print_parameter_explanations() {
println!(" • Default (400×200): Good balance");
println!(" • Larger (800×400): Slower, more detail");

println!("\n --dither-mode <MODE> (default: none)");
println!("\n --dither-mode <MODE> (default: none) [dev-only]");
println!(" Dithering algorithm for color quantization.");
println!(" • none: No dithering");
println!(" • ordered: Bayer matrix ordered dithering");
Expand Down Expand Up @@ -242,8 +245,11 @@ pub fn print_parameter_explanations() {
println!("\n # Chaotic exploration");
println!(" tslime --sensor-angle 45 --rotation-angle 60 --sensor-distance 15");

println!("\n # Multi-species competition");
println!(" tslime --species 'red:20k:ff0000,blue:20k:0000ff' --separate-species-trails");
#[cfg(feature = "multi-species")]
{
println!("\n # Multi-species competition");
println!(" tslime --species 'red:20k:ff0000,blue:20k:0000ff' --separate-species-trails");
}

println!("\n # Wind-driven river pattern");
println!(" tslime --preset river --wind 0.3,0.0");
Expand Down
16 changes: 8 additions & 8 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ pub fn print_mode(
config.max_brightness
};

let species_rgb_colors = if args.species_colors {
let species_rgb_colors = if args.species_colors_enabled() {
Some(extract_species_rgb_colors(&config))
} else {
None
Expand All @@ -502,7 +502,7 @@ pub fn print_mode(
dither_mode,
&mut None,
intensity_mapping.as_ref(),
args.species_colors,
args.species_colors_enabled(),
species_rgb_colors,
background_color,
args.ascii_contrast,
Expand Down Expand Up @@ -637,7 +637,7 @@ pub fn capture_frames_mode(
config.max_brightness
};

let species_rgb_colors = if args.species_colors {
let species_rgb_colors = if args.species_colors_enabled() {
Some(extract_species_rgb_colors(&config))
} else {
None
Expand All @@ -660,7 +660,7 @@ pub fn capture_frames_mode(
args.dither_mode().unwrap_or(DitherMode::None),
&mut None,
intensity_mapping.as_ref(),
args.species_colors,
args.species_colors_enabled(),
species_rgb_colors,
background_color,
args.ascii_contrast,
Expand Down Expand Up @@ -829,7 +829,7 @@ pub fn export_gif_mode(
config.max_brightness
};

let species_rgb_colors = if args.species_colors {
let species_rgb_colors = if args.species_colors_enabled() {
Some(extract_species_rgb_colors(&config))
} else {
None
Expand All @@ -852,7 +852,7 @@ pub fn export_gif_mode(
args.dither_mode().unwrap_or(DitherMode::None),
&mut None,
intensity_mapping.as_ref(),
args.species_colors,
args.species_colors_enabled(),
species_rgb_colors,
background_color,
args.ascii_contrast,
Expand Down Expand Up @@ -957,7 +957,7 @@ pub fn export_webm_mode(
config.max_brightness
};

let species_rgb_colors = if args.species_colors {
let species_rgb_colors = if args.species_colors_enabled() {
Some(extract_species_rgb_colors(&config))
} else {
None
Expand All @@ -980,7 +980,7 @@ pub fn export_webm_mode(
args.dither_mode().unwrap_or(DitherMode::None),
&mut None,
intensity_mapping.as_ref(),
args.species_colors,
args.species_colors_enabled(),
species_rgb_colors,
background_color,
args.ascii_contrast,
Expand Down
90 changes: 49 additions & 41 deletions src/app/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ pub fn run_simulation(
);
let dither_mode = args.dither_mode().unwrap_or(DitherMode::None);
renderer.set_dither_mode(dither_mode);
// Dither is dev-only for v0.1.0: runtime keys work only when it was
// explicitly enabled at startup via the (hidden) CLI flags.
let dither_unlocked = !matches!(dither_mode, DitherMode::None);
renderer.set_ascii_contrast(args.ascii_contrast);
renderer.set_window_frame(config.window_frame);
let mut timer = FrameTimer::with_time_scale(args.fps, args.frame_delay, args.time_scale);
Expand Down Expand Up @@ -330,7 +333,7 @@ pub fn run_simulation(
}

// let config = args.to_sim_config().unwrap(); // Already parsed above
if args.species_colors {
if args.species_colors_enabled() {
let species_rgb_colors = extract_species_rgb_colors(&config);
renderer.set_species_colors(true, species_rgb_colors);
}
Expand Down Expand Up @@ -411,13 +414,6 @@ pub fn run_simulation(
} else {
None
};
#[cfg(not(feature = "audio"))]
{
if args.choir {
eprintln!("--choir requires building with --features audio");
}
}

loop {
if is_shutdown_requested() {
break;
Expand Down Expand Up @@ -1113,7 +1109,7 @@ pub fn run_simulation(
});
}

if args.species_colors && sim.config().separate_species_trails {
if args.species_colors_enabled() && sim.config().separate_species_trails {
let species_trail_maps = sim.trail_maps_for_species_colors();
let species_rgb_colors = extract_species_rgb_colors(&current_config);
let combined: Vec<_> = species_trail_maps
Expand Down Expand Up @@ -1582,16 +1578,37 @@ pub fn run_simulation(
));
}
ControlAction::ToggleDither => {
runtime_state.toggle_dither();
renderer.set_dither_mode(runtime_state.dither_mode);
if dither_unlocked {
runtime_state.toggle_dither();
renderer.set_dither_mode(runtime_state.dither_mode);
} else {
runtime_state.show_notification(
"Dither is dev-only - see help-wanted issues on GitHub"
.to_string(),
);
}
}
ControlAction::CycleDitherMode => {
runtime_state.cycle_dither_mode();
renderer.set_dither_mode(runtime_state.dither_mode);
if dither_unlocked {
runtime_state.cycle_dither_mode();
renderer.set_dither_mode(runtime_state.dither_mode);
} else {
runtime_state.show_notification(
"Dither is dev-only - see help-wanted issues on GitHub"
.to_string(),
);
}
}
ControlAction::AdjustDitherIntensity(delta) => {
runtime_state.adjust_dither_intensity(delta);
renderer.set_dither_mode(runtime_state.dither_mode);
if dither_unlocked {
runtime_state.adjust_dither_intensity(delta);
renderer.set_dither_mode(runtime_state.dither_mode);
} else {
runtime_state.show_notification(
"Dither is dev-only - see help-wanted issues on GitHub"
.to_string(),
);
}
}
ControlAction::ToggleKeyboardHints => {
runtime_state.toggle_keyboard_hints();
Expand Down Expand Up @@ -2074,35 +2091,26 @@ pub fn run_simulation(
renderer.set_window_layout(None);
}
}
#[cfg(feature = "audio")]
ControlAction::ToggleChoir => {
#[cfg(feature = "audio")]
{
if choir.is_some() {
choir = None;
runtime_state.show_notification("Choir mode: off".to_string());
} else {
match crate::audio::Choir::try_new(
args.choir_volume.clamp(0.0, 1.0),
) {
Ok(c) => {
choir = Some(c);
runtime_state
.show_notification("Choir mode: on".to_string());
}
Err(e) => {
runtime_state.show_notification(format!(
"Choir init failed: {e}"
));
}
if choir.is_some() {
choir = None;
runtime_state.show_notification("Choir mode: off".to_string());
} else {
match crate::audio::Choir::try_new(
args.choir_volume.clamp(0.0, 1.0),
) {
Ok(c) => {
choir = Some(c);
runtime_state
.show_notification("Choir mode: on".to_string());
}
Err(e) => {
runtime_state
.show_notification(format!("Choir init failed: {e}"));
}
}
}
#[cfg(not(feature = "audio"))]
{
runtime_state.show_notification(
"Choir requires --features audio".to_string(),
);
}
}
ControlAction::None => {}
}
Expand Down Expand Up @@ -2287,7 +2295,7 @@ pub fn run_simulation(
};

// Re-render with updated pause state
if args.species_colors && sim.config().separate_species_trails {
if args.species_colors_enabled() && sim.config().separate_species_trails {
let species_trail_maps = sim.trail_maps_for_species_colors();
let species_rgb_colors = extract_species_rgb_colors(&current_config);
let combined: Vec<_> = species_trail_maps
Expand Down
Loading
Loading