1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct LifeConfig {
6 pub heat_decay: f32,
8 pub zone_width: usize,
10 pub zone_height: usize,
12 pub check_interval: u64,
14 pub target_activity: f32,
16 pub injection_gain: f32,
18 pub max_injection_chance: f32,
20 pub zone_cooldown_min: u32,
22 pub zone_cooldown_max: u32,
24 pub mutation_chance: f32,
26 pub initial_soup_density: f32,
28 pub prune_interval: u64,
30 pub prune_density_threshold: f32,
32 pub prune_percentage: f32,
34 pub center_bias: f32,
36 pub rebalance_interval: u64,
38}
39
40impl Default for LifeConfig {
41 fn default() -> Self {
42 Self {
43 heat_decay: 0.98,
44 zone_width: 8, zone_height: 8, check_interval: 64,
47 target_activity: 0.005, injection_gain: 5.0,
49 max_injection_chance: 0.05,
50 zone_cooldown_min: 800,
51 zone_cooldown_max: 4000,
52 mutation_chance: 0.05,
53 initial_soup_density: 0.01, prune_interval: 30, prune_density_threshold: 0.25, prune_percentage: 0.45, center_bias: 0.3, rebalance_interval: 100, }
60 }
61}
62
63#[derive(Debug, Clone, Copy, Default)]
65pub struct ZoneStats {
66 pub heat: f32,
68 pub density: f32,
70 pub cooldown: u32,
72 pub changed_count: usize,
74 pub last_injection_generation: u64,
76}
77
78impl ZoneStats {
79 pub fn record_change(&mut self) {
81 self.changed_count += 1;
82 }
83
84 pub fn decay_heat(&mut self, decay: f32) {
86 self.heat *= decay;
87 }
88
89 pub fn update_density(&mut self, alive: usize, total: usize) {
91 self.density = if total == 0 {
92 0.0
93 } else {
94 alive as f32 / total as f32
95 };
96 }
97
98 pub fn is_cool(&self) -> bool {
100 self.cooldown == 0
101 }
102
103 pub fn tick_cooldown(&mut self) {
105 if self.cooldown > 0 {
106 self.cooldown -= 1;
107 }
108 }
109
110 pub fn set_cooldown(&mut self, min: u32, max: u32, rng: &mut impl rand::Rng) {
112 self.cooldown = rng.random_range(min..=max);
113 }
114
115 pub fn reset_changed(&mut self) {
117 self.changed_count = 0;
118 }
119
120 pub fn add_heat(&mut self, amount: f32) {
122 self.heat = (self.heat + amount).min(1.0);
123 }
124 pub fn is_on_cooldown(&self) -> bool {
126 self.cooldown > 0
127 }
128
129 pub fn is_cold_empty(&self) -> bool {
131 self.heat < 0.1 && self.density < 0.05
132 }
133
134 pub fn is_cold_ash(&self) -> bool {
136 self.heat < 0.1 && self.density >= 0.05 && self.density < 0.5
137 }
138}
139
140#[derive(Debug)]
142pub struct LifeField {
143 pub(super) width: usize,
145 pub(super) height: usize,
147 pub(super) cells: Vec<bool>,
149 low_activity_ticks: usize,
151 injection_phase: usize,
153 config: LifeConfig,
155 rng: StdRng,
157 heatmap: Vec<f32>,
159 zones: Vec<ZoneStats>,
161 generation: u64,
163 last_changed_cells: Vec<(usize, usize)>,
165}
166
167impl Default for LifeField {
168 fn default() -> Self {
169 Self {
170 width: 0,
171 height: 0,
172 cells: Vec::new(),
173 low_activity_ticks: 0,
174 injection_phase: 0,
175 config: LifeConfig::default(),
176 rng: StdRng::from_rng(&mut rand::rng()),
177 heatmap: Vec::new(),
178 zones: Vec::new(),
179 generation: 0,
180 last_changed_cells: Vec::new(),
181 }
182 }
183}
184
185impl LifeField {
186 pub fn from_seed(prompt: &str, width: usize, height: usize) -> Self {
188 let seed = hash_seed(prompt, width, height, "life-generator-v1");
189 let mut field = Self {
190 width,
191 height,
192 cells: vec![false; width * height],
193 low_activity_ticks: 0,
194 injection_phase: 0,
195 config: LifeConfig::default(),
196 rng: StdRng::from_seed(seed),
197 heatmap: vec![0.0; width * height],
198 zones: Vec::new(),
199 generation: 0,
200 last_changed_cells: Vec::new(),
201 };
202 field.initialize_zones();
203 field.seed_from_rng();
204 field
205 }
206
207 fn initialize_zones(&mut self) {
208 let zone_cols = (self.width + self.config.zone_width - 1) / self.config.zone_width;
210 let zone_rows = (self.height + self.config.zone_height - 1) / self.config.zone_height;
211 self.zones = vec![ZoneStats::default(); zone_cols * zone_rows];
212 }
213
214 fn seed_from_rng(&mut self) {
215 use rand::prelude::IndexedRandom;
216
217 let width = self.width;
218 let height = self.height;
219
220 let density = self.config.initial_soup_density;
222 for y in 0..height {
223 for x in 0..width {
224 if self.rng.random::<f32>() < density {
225 let idx = y * width + x;
226 if idx < self.cells.len() {
227 self.cells[idx] = true;
228 }
229 }
230 }
231 }
232
233 let methuselahs = [&R_PENTOMINO_PATTERN, &ACORN_PATTERN];
235 let num_methuselahs = self.rng.random_range(2..=5);
236 for _ in 0..num_methuselahs {
237 let pattern = methuselahs.choose(&mut self.rng).unwrap();
238 let x = self.rng.random_range(0..width);
239 let y = self.rng.random_range(0..height);
240 let rotation = self.rng.random_range(0..4);
241 let flip = self.rng.random_bool(0.5);
242
243 let placement = PatternPlacement {
244 pattern: **pattern,
245 dx: x as isize,
246 dy: y as isize,
247 rotation,
248 flip,
249 };
250 self.place_pattern(pattern, &placement);
251 }
252
253 let moving = [&GLIDER_PATTERN, &LWSS_PATTERN];
257 let num_pairs = self.rng.random_range(2..=4); for _ in 0..num_pairs {
260 let pattern = moving.choose(&mut self.rng).unwrap();
261
262 let x1 = self.rng.random_range(0..width);
264 let y1 = self.rng.random_range(0..height);
265 let rot1 = self.rng.random_range(0..4);
267 let flip1 = self.rng.random_bool(0.5);
268
269 let x2 = (width - x1) % width;
271 let y2 = (height - y1) % height;
272 let rot2 = (rot1 + 2) % 4;
275 let flip2 = flip1; let placement1 = PatternPlacement {
279 pattern: **pattern,
280 dx: x1 as isize,
281 dy: y1 as isize,
282 rotation: rot1,
283 flip: flip1,
284 };
285 self.place_pattern(pattern, &placement1);
286
287 let placement2 = PatternPlacement {
289 pattern: **pattern,
290 dx: x2 as isize,
291 dy: y2 as isize,
292 rotation: rot2,
293 flip: flip2,
294 };
295 self.place_pattern(pattern, &placement2);
296 }
297
298 let num_blobs = self.rng.random_range(2..=4);
300 for _ in 0..num_blobs {
301 self.place_random_blob();
302 }
303 }
304
305 fn place_random_blob(&mut self) {
307 let blob_size = self.rng.random_range(4..=6);
308 let x = self
309 .rng
310 .random_range(0..self.width.saturating_sub(blob_size));
311 let y = self
312 .rng
313 .random_range(0..self.height.saturating_sub(blob_size));
314
315 for dy in 0..blob_size {
317 for dx in 0..blob_size {
318 if self.rng.random_bool(0.5) {
319 let px = x + dx;
320 let py = y + dy;
321 if px < self.width && py < self.height {
322 let idx = py * self.width + px;
323 if idx < self.cells.len() {
324 self.cells[idx] = true;
325 }
326 }
327 }
328 }
329 }
330 }
331
332 pub fn ensure_size(&mut self, width: usize, height: usize) {
338 let width = width.max(2);
339 let height = height.max(4);
340 if self.width == width && self.height == height && !self.cells.is_empty() {
341 return;
342 }
343
344 self.width = width;
345 self.height = height;
346 self.cells = vec![false; width * height];
347 self.heatmap = vec![0.0; width * height];
348 self.low_activity_ticks = 0;
349 self.injection_phase = 0;
350 self.generation = 0;
351 self.last_changed_cells.clear();
352
353 self.initialize_zones();
355 self.seed_from_rng();
356 }
357
358 pub fn step(&mut self) {
368 if self.width == 0 || self.height == 0 || self.cells.is_empty() {
369 return;
370 }
371
372 let old_cells = self.cells.clone();
374
375 self.last_changed_cells.clear();
377
378 let width = self.width;
380 let height = self.height;
381 let mut next = vec![false; width * height];
382
383 for y in 0..height {
384 for x in 0..width {
385 let idx = y * width + x;
386 let alive = self.cells[idx];
387 let neighbors = self.live_neighbor_count(x, y);
388
389 let next_alive = match (alive, neighbors) {
390 (true, 2) | (true, 3) => true,
391 (true, 6) => true, (false, 3) => true,
393 _ => false,
394 };
395
396 next[idx] = next_alive;
397 if alive != next_alive {
398 self.last_changed_cells.push((x, y));
399 }
400 }
401 }
402
403 self.cells = next;
404 self.generation += 1;
405
406 self.update_heatmap(&old_cells);
408
409 self.update_zones();
411
412 self.decay_cooldowns();
414
415 self.maybe_inject();
417
418 self.rebalance();
420
421 if self.generation % self.config.prune_interval == 0 {
423 self.prune_dense_areas();
424 }
425 }
426
427 pub fn live_neighbor_count(&self, x: usize, y: usize) -> u8 {
438 let mut count = 0u8;
439 for dy in [-1isize, 0, 1] {
440 for dx in [-1isize, 0, 1] {
441 if dx == 0 && dy == 0 {
442 continue;
443 }
444 let nx = wrap_index(x, dx, self.width);
445 let ny = wrap_index(y, dy, self.height);
446 if self.cells[self.index(nx, ny)] {
447 count += 1;
448 }
449 }
450 }
451 count
452 }
453
454 pub fn index(&self, x: usize, y: usize) -> usize {
463 y * self.width + x
464 }
465
466 pub fn update_heatmap(&mut self, old_cells: &[bool]) {
469 let decay = self.config.heat_decay;
470 for y in 0..self.height {
471 for x in 0..self.width {
472 let idx = self.index(x, y);
473 let changed = old_cells[idx] != self.cells[idx];
474 let current_heat = self.heatmap[idx];
475 self.heatmap[idx] = current_heat * decay + if changed { 1.0 } else { 0.0 };
476 }
477 }
478 }
479
480 pub fn update_zones(&mut self) {
482 let zone_cols = (self.width + self.config.zone_width - 1) / self.config.zone_width;
483 let zone_rows = (self.height + self.config.zone_height - 1) / self.config.zone_height;
484
485 for zy in 0..zone_rows {
486 for zx in 0..zone_cols {
487 let zone_idx = zy * zone_cols + zx;
488 let zone = &mut self.zones[zone_idx];
489
490 let x_start = zx * self.config.zone_width;
492 let y_start = zy * self.config.zone_height;
493 let x_end = (x_start + self.config.zone_width).min(self.width);
494 let y_end = (y_start + self.config.zone_height).min(self.height);
495
496 let mut total_heat = 0.0;
498 let mut live_cells = 0;
499 let mut changed_cells = 0;
500 let zone_area = (x_end - x_start) * (y_end - y_start);
501
502 for y in y_start..y_end {
503 for x in x_start..x_end {
504 let idx = y * self.width + x; total_heat += self.heatmap[idx];
506 if self.cells[idx] {
507 live_cells += 1;
508 }
509 if self.last_changed_cells.contains(&(x, y)) {
511 changed_cells += 1;
512 }
513 }
514 }
515
516 zone.heat = (total_heat / zone_area as f32).min(5.0); zone.density = live_cells as f32 / zone_area as f32;
518 zone.changed_count = changed_cells;
519 }
520 }
521 }
522
523 pub fn decay_cooldowns(&mut self) {
525 for zone in &mut self.zones {
526 if zone.cooldown > 0 {
527 zone.cooldown -= 1;
528 }
529 }
530 }
531
532 pub fn choose_injection_zone(&mut self) -> Option<usize> {
535 let zone_cols = (self.width + self.config.zone_width - 1) / self.config.zone_width;
536 let zone_rows = (self.height + self.config.zone_height - 1) / self.config.zone_height;
537
538 let mut weights: Vec<(usize, f32)> = Vec::new();
539
540 for zy in 0..zone_rows {
541 for zx in 0..zone_cols {
542 let zone_idx = zy * zone_cols + zx;
543 let zone = &self.zones[zone_idx];
544
545 if zone.is_on_cooldown() {
547 continue;
548 }
549
550 let coldness = 1.0 - (zone.heat / 5.0).min(1.0);
553
554 let neighbor_heat = self.get_neighbor_zone_heat(zx, zy, zone_cols, zone_rows);
556
557 let density_suitability = if zone.density < 0.05 {
559 0.3 } else if zone.density > 0.8 {
561 0.1 } else {
563 1.0 - (zone.density - 0.4).abs() };
565
566 let center_x = (zone_cols - 1) as f32 / 2.0;
568 let center_y = (zone_rows - 1) as f32 / 2.0;
569 let dist_from_center =
570 ((zx as f32 - center_x).powi(2) + (zy as f32 - center_y).powi(2)).sqrt();
571 let max_dist = ((center_x.powi(2) + center_y.powi(2)).sqrt()).max(1.0);
572 let center_factor = 1.0 - (dist_from_center / max_dist) * self.config.center_bias;
573
574 let weight =
575 coldness * (0.5 + neighbor_heat * 0.5) * density_suitability * center_factor;
576
577 if weight > 0.01 {
578 weights.push((zone_idx, weight));
579 }
580 }
581 }
582
583 if weights.is_empty() {
584 return None;
585 }
586
587 let total_weight: f32 = weights.iter().map(|(_, w)| w).sum();
589 let mut choice = self.rng.random::<f32>() * total_weight;
590
591 let last_idx = weights.last().unwrap().0;
593
594 for (idx, weight) in weights {
595 choice -= weight;
596 if choice <= 0.0 {
597 return Some(idx);
598 }
599 }
600
601 Some(last_idx)
602 }
603
604 fn get_neighbor_zone_heat(&self, zx: usize, zy: usize, cols: usize, rows: usize) -> f32 {
606 let mut total_heat = 0.0;
607 let mut count = 0;
608
609 for dy in [-1, 0, 1] {
610 for dx in [-1, 0, 1] {
611 if dx == 0 && dy == 0 {
612 continue;
613 }
614 let nx = zx as isize + dx;
616 let ny = zy as isize + dy;
617
618 if nx < 0 || nx >= cols as isize || ny < 0 || ny >= rows as isize {
620 continue;
621 }
622
623 let nidx = ny as usize * cols + nx as usize;
624 if nidx < self.zones.len() {
625 total_heat += self.zones[nidx].heat;
626 count += 1;
627 }
628 }
629 }
630
631 if count > 0 {
632 total_heat / count as f32
633 } else {
634 0.0
635 }
636 }
637
638 fn global_density(&self) -> f32 {
640 let live_cells = self.cells.iter().filter(|&&c| c).count();
641 live_cells as f32 / self.cells.len() as f32
642 }
643
644 pub fn maybe_inject(&mut self) {
646 if self.generation % self.config.check_interval != 0 {
648 return;
649 }
650
651 let global_density = self.global_density();
653 if global_density > 0.15 {
654 return;
656 }
657
658 let total_cells = self.width * self.height;
660 let changed_cells: usize = self.zones.iter().map(|z| z.changed_count).sum();
661 let global_activity = changed_cells as f32 / total_cells as f32;
662
663 let deficit = (self.config.target_activity - global_activity).max(0.0);
665 let chance = (deficit * self.config.injection_gain).min(self.config.max_injection_chance);
666
667 if self.rng.random::<f32>() >= chance {
669 return;
670 }
671
672 let Some(zone_idx) = self.choose_injection_zone() else {
674 return;
675 };
676
677 let zone_cols = (self.width + self.config.zone_width - 1) / self.config.zone_width;
679 let zy = zone_idx / zone_cols;
680 let zx = zone_idx % zone_cols;
681
682 let pattern = self.choose_pattern_for_zone(zone_idx);
684 let placement = self.choose_placement(zx, zy, &pattern);
685
686 self.place_pattern(&pattern, &placement);
688
689 self.zones[zone_idx].last_injection_generation = self.generation;
691
692 let cooldown_range = self.config.zone_cooldown_min..=self.config.zone_cooldown_max;
694 let cooldown = self.rng.random_range(cooldown_range);
695 self.zones[zone_idx].cooldown = cooldown;
696 }
697
698 fn choose_pattern_for_zone(&mut self, zone_idx: usize) -> &'static SeedPattern {
700 let zone = &self.zones[zone_idx];
701
702 const METHUSELAHS: &[&SeedPattern] = &[&R_PENTOMINO_PATTERN, &ACORN_PATTERN];
704 const MOVING: &[&SeedPattern] = &[&GLIDER_PATTERN, &LWSS_PATTERN];
705 const RANDOM_BLOBS: &[&SeedPattern] =
706 &[&RANDOM_BLOB_4X4, &RANDOM_BLOB_5X5, &RANDOM_BLOB_6X6];
707
708 let choices: Vec<&SeedPattern> = if zone.is_cold_empty() {
709 METHUSELAHS
711 .iter()
712 .chain(MOVING.iter())
713 .chain(RANDOM_BLOBS.iter())
714 .copied()
715 .collect()
716 } else if zone.is_cold_ash() {
717 MOVING.iter().chain(RANDOM_BLOBS.iter()).copied().collect()
719 } else {
720 vec![
722 &GLIDER_PATTERN,
723 &R_PENTOMINO_PATTERN,
724 &ACORN_PATTERN,
725 &LWSS_PATTERN,
726 &RANDOM_BLOB_4X4,
727 &RANDOM_BLOB_5X5,
728 &RANDOM_BLOB_6X6,
729 ]
730 };
731
732 let idx = self.rng.random_range(0..choices.len().max(1));
734 choices.get(idx).copied().unwrap_or(&GLIDER_PATTERN)
735 }
736
737 fn choose_placement(
739 &mut self,
740 zx: usize,
741 zy: usize,
742 pattern: &SeedPattern,
743 ) -> PatternPlacement {
744 let x_start = zx * self.config.zone_width;
746 let y_start = zy * self.config.zone_height;
747 let x_end = (x_start + self.config.zone_width).min(self.width);
748 let y_end = (y_start + self.config.zone_height).min(self.height);
749
750 let margin_x = pattern.width.min(4);
752 let margin_y = pattern.height.min(4);
753 let x = if x_end > x_start + margin_x * 2 {
754 self.rng.random_range(x_start + margin_x..x_end - margin_x)
755 } else {
756 x_start
757 };
758 let y = if y_end > y_start + margin_y * 2 {
759 self.rng.random_range(y_start + margin_y..y_end - margin_y)
760 } else {
761 y_start
762 };
763
764 let rotation = self.rng.random_range(0..4) as u8;
766 let flip = self.rng.random::<f32>() < 0.5;
767
768 PatternPlacement {
769 pattern: *pattern,
770 dx: x as isize,
771 dy: y as isize,
772 rotation,
773 flip,
774 }
775 }
776
777 fn place_pattern(&mut self, pattern: &SeedPattern, placement: &PatternPlacement) {
779 for &(px, py) in pattern.cells {
780 let (rx, ry) = rotate_cell(px, py, pattern.width, pattern.height, placement.rotation);
782 let (fx, fy) = if placement.flip {
783 (pattern.width - 1 - rx, ry)
784 } else {
785 (rx, ry)
786 };
787
788 let x = wrap_index_signed(placement.dx + fx as isize, self.width);
790 let y = wrap_index_signed(placement.dy + fy as isize, self.height);
791 let idx = y * self.width + x;
792
793 let mutation = self.rng.random::<f32>() < self.config.mutation_chance;
795 if mutation {
796 if self.rng.random::<f32>() < 0.5 {
798 continue; } else {
800 if idx < self.cells.len() {
802 self.cells[idx] = true;
803 }
804 if self.rng.random::<f32>() < 0.3 {
806 let dx = self.rng.random_range(-1i32..=1) as isize;
807 let dy = self.rng.random_range(-1i32..=1) as isize;
808 let nx = wrap_index_signed(x as isize + dx, self.width);
809 let ny = wrap_index_signed(y as isize + dy, self.height);
810 let nidx = ny * self.width + nx;
811 if nidx < self.cells.len() {
812 self.cells[nidx] = true;
813 }
814 }
815 continue;
816 }
817 }
818
819 if idx < self.cells.len() {
821 self.cells[idx] = true;
822 }
823 }
824 }
825
826 fn prune_dense_areas(&mut self) {
828 use rand::seq::IteratorRandom;
829
830 let global_density = self.global_density();
831 let zone_cols = (self.width + self.config.zone_width - 1) / self.config.zone_width;
832 let zone_rows = (self.height + self.config.zone_height - 1) / self.config.zone_height;
833
834 if global_density > 0.15 {
837 let total_cells = self.width * self.height;
838 let target_cells = (total_cells as f32 * 0.12) as usize; let current_live: usize = self.cells.iter().filter(|&&c| c).count();
840
841 if current_live > target_cells {
842 let to_remove_total = current_live - target_cells;
843 let all_live: Vec<usize> = self
845 .cells
846 .iter()
847 .enumerate()
848 .filter(|(_, &c)| c)
849 .map(|(i, _)| i)
850 .collect();
851
852 let mut rng = &mut self.rng;
853 for idx in all_live
854 .iter()
855 .sample(&mut rng, to_remove_total.min(all_live.len()))
856 {
857 self.cells[*idx] = false;
858 }
859 }
860 return;
861 }
862
863 for zy in 0..zone_rows {
865 for zx in 0..zone_cols {
866 let zone_idx = zy * zone_cols + zx;
867 let zone = &self.zones[zone_idx];
868
869 if zone.density < self.config.prune_density_threshold {
870 continue;
871 }
872
873 let x_start = zx * self.config.zone_width;
875 let y_start = zy * self.config.zone_height;
876 let x_end = (x_start + self.config.zone_width).min(self.width);
877 let y_end = (y_start + self.config.zone_height).min(self.height);
878
879 let mut live_cells: Vec<(usize, usize)> = Vec::new();
881 for y in y_start..y_end {
882 for x in x_start..x_end {
883 let idx = y * self.width + x;
884 if self.cells[idx] {
885 live_cells.push((x, y));
886 }
887 }
888 }
889
890 let to_remove = (live_cells.len() as f32 * self.config.prune_percentage) as usize;
892 let mut rng = &mut self.rng;
893 for (x, y) in live_cells.iter().sample(&mut rng, to_remove) {
894 let idx = y * self.width + x;
895 self.cells[idx] = false;
896 }
897 }
898 }
899 }
900
901 fn rebalance(&mut self) {
903 if self.generation % self.config.rebalance_interval != 0 {
904 return;
905 }
906
907 let third_width = self.width / 3;
909 let left_count = self.count_cells_in_region(0, third_width);
910 let center_count = self.count_cells_in_region(third_width, third_width * 2);
911 let right_count = self.count_cells_in_region(third_width * 2, self.width);
912
913 let total = left_count + center_count + right_count;
914 if total == 0 {
915 return;
916 }
917
918 let left_pct = left_count as f32 / total as f32;
919 let center_pct = center_count as f32 / total as f32;
920 let right_pct = right_count as f32 / total as f32;
921
922 let threshold = 0.40;
924 if left_pct > threshold {
925 let to_prune = ((left_pct - 0.33) * total as f32) as usize;
926 self.prune_region(0, third_width, to_prune);
927 }
928 if center_pct > threshold {
929 let to_prune = ((center_pct - 0.33) * total as f32) as usize;
930 self.prune_region(third_width, third_width * 2, to_prune);
931 }
932 if right_pct > threshold {
933 let to_prune = ((right_pct - 0.33) * total as f32) as usize;
934 self.prune_region(third_width * 2, self.width, to_prune);
935 }
936 }
937
938 fn count_cells_in_region(&self, x_start: usize, x_end: usize) -> usize {
940 let x_start = x_start.min(self.width);
941 let x_end = x_end.min(self.width);
942 if x_start >= x_end {
943 return 0;
944 }
945
946 let mut count = 0;
947 for y in 0..self.height {
948 for x in x_start..x_end {
949 let idx = y * self.width + x;
950 if self.cells[idx] {
951 count += 1;
952 }
953 }
954 }
955 count
956 }
957
958 fn prune_region(&mut self, x_start: usize, x_end: usize, count: usize) {
960 use rand::seq::IteratorRandom;
961
962 let x_start = x_start.min(self.width);
963 let x_end = x_end.min(self.width);
964 if x_start >= x_end || count == 0 {
965 return;
966 }
967
968 let mut live_cells: Vec<usize> = Vec::new();
970 for y in 0..self.height {
971 for x in x_start..x_end {
972 let idx = y * self.width + x;
973 if self.cells[idx] {
974 live_cells.push(idx);
975 }
976 }
977 }
978
979 let to_remove = count.min(live_cells.len());
981 let mut rng = &mut self.rng;
982 for idx in live_cells.iter().sample(&mut rng, to_remove) {
983 self.cells[*idx] = false;
984 }
985 }
986
987 pub fn generation(&self) -> u64 {
989 self.generation
990 }
991
992 pub fn dimensions(&self) -> (usize, usize) {
994 (self.width, self.height)
995 }
996
997 pub fn live_cells(&self) -> Vec<(usize, usize)> {
999 let mut cells = Vec::new();
1000 for y in 0..self.height {
1001 for x in 0..self.width {
1002 let idx = y * self.width + x;
1003 if idx < self.cells.len() && self.cells[idx] {
1004 cells.push((x, y));
1005 }
1006 }
1007 }
1008 cells
1009 }
1010
1011 pub fn live_cell_count(&self) -> usize {
1013 self.cells.iter().filter(|&&c| c).count()
1014 }
1015
1016 pub fn zone_stats(&self) -> Vec<((usize, usize), ZoneStats)> {
1018 let zone_cols = (self.width + self.config.zone_width - 1) / self.config.zone_width;
1019 let zone_rows = (self.height + self.config.zone_height - 1) / self.config.zone_height;
1020
1021 let mut result = Vec::new();
1022 for zy in 0..zone_rows {
1023 for zx in 0..zone_cols {
1024 let zone_idx = zy * zone_cols + zx;
1025 if zone_idx < self.zones.len() {
1026 result.push(((zx, zy), self.zones[zone_idx]));
1027 }
1028 }
1029 }
1030 result
1031 }
1032
1033 pub fn config(&self) -> &LifeConfig {
1035 &self.config
1036 }
1037}