Skip to main content

proof_engine/entity/
formation.rs

1//! Target formation shapes for amorphous entities.
2//!
3//! Every formation is a mathematical function mapping index → 3D offset.
4//! Formations are composable: scale, rotate, translate, and join operations
5//! allow building complex shapes from simple primitives.
6
7use glam::Vec3;
8use std::f32::consts::{TAU, PI};
9
10/// A formation: a list of 3D offsets from the entity center with per-slot characters.
11#[derive(Clone, Debug)]
12pub struct Formation {
13    pub positions: Vec<Vec3>,
14    pub chars: Vec<char>,
15}
16
17// ── Primitive formations ───────────────────────────────────────────────────────
18
19impl Formation {
20    /// A single glyph at the origin.
21    pub fn single(ch: char) -> Self {
22        Self { positions: vec![Vec3::ZERO], chars: vec![ch] }
23    }
24
25    /// A 3×3 grid (9 glyphs).
26    pub fn grid_3x3() -> Self {
27        Self::grid(3, 3, 1.0)
28    }
29
30    /// An N×M grid with given spacing.
31    pub fn grid(cols: i32, rows: i32, spacing: f32) -> Self {
32        let mut positions = Vec::new();
33        let mut chars = Vec::new();
34        let cx = (cols - 1) as f32 * spacing * 0.5;
35        let cy = (rows - 1) as f32 * spacing * 0.5;
36        for row in 0..rows {
37            for col in 0..cols {
38                let x = col as f32 * spacing - cx;
39                let y = row as f32 * spacing - cy;
40                positions.push(Vec3::new(x, y, 0.0));
41                chars.push(if col == cols / 2 && row == rows / 2 { '@' } else { '#' });
42            }
43        }
44        Self { positions, chars }
45    }
46
47    /// A circle of N glyphs at the given radius.
48    pub fn circle(n: usize, radius: f32) -> Self {
49        let mut positions = Vec::new();
50        let mut chars = Vec::new();
51        for i in 0..n {
52            let angle = i as f32 / n as f32 * TAU;
53            positions.push(Vec3::new(angle.cos() * radius, angle.sin() * radius, 0.0));
54            chars.push('◆');
55        }
56        Self { positions, chars }
57    }
58
59    /// Concentric rings. Each entry is (glyph_count, radius).
60    pub fn rings(spec: &[(usize, f32)]) -> Self {
61        let ring_chars = ['◆', '◇', '◈', '◉', '·', '·'];
62        let mut positions = Vec::new();
63        let mut chars = Vec::new();
64        // Center glyph
65        positions.push(Vec3::ZERO);
66        chars.push('◈');
67        for (ri, &(n, radius)) in spec.iter().enumerate() {
68            let ch = ring_chars[ri.min(ring_chars.len() - 1)];
69            for i in 0..n {
70                let angle = i as f32 / n as f32 * TAU;
71                positions.push(Vec3::new(angle.cos() * radius, angle.sin() * radius, 0.0));
72                chars.push(ch);
73            }
74        }
75        Self { positions, chars }
76    }
77
78    /// Diamond shape (all integer (x,y) within Manhattan distance ≤ size).
79    pub fn diamond(size: i32) -> Self {
80        let mut positions = Vec::new();
81        let mut chars = Vec::new();
82        for y in -size..=size {
83            for x in -size..=size {
84                if x.abs() + y.abs() <= size {
85                    positions.push(Vec3::new(x as f32, y as f32, 0.0));
86                    chars.push(if x == 0 && y == 0 { '◈' } else { '◇' });
87                }
88            }
89        }
90        Self { positions, chars }
91    }
92
93    /// Cross / plus sign with arm_length glyphs per arm (center + 4 arms).
94    pub fn cross(arm_length: i32) -> Self {
95        let mut positions = Vec::new();
96        let mut chars = Vec::new();
97        positions.push(Vec3::ZERO);
98        chars.push('╬');
99        for i in 1..=arm_length {
100            for &(dx, dy, ch) in &[(1i32,0i32,'═'),(-1,0,'═'),(0,1,'║'),(0,-1,'║')] {
101                positions.push(Vec3::new(dx as f32 * i as f32, dy as f32 * i as f32, 0.0));
102                chars.push(ch);
103            }
104        }
105        Self { positions, chars }
106    }
107
108    /// N-pointed star with inner_r and outer_r radii.
109    pub fn star(points: usize, inner_r: f32, outer_r: f32) -> Self {
110        let mut positions = Vec::new();
111        let mut chars = Vec::new();
112        let n = points * 2;
113        for i in 0..n {
114            let angle = i as f32 / n as f32 * TAU - PI / 2.0;
115            let r = if i % 2 == 0 { outer_r } else { inner_r };
116            positions.push(Vec3::new(angle.cos() * r, angle.sin() * r, 0.0));
117            chars.push(if i % 2 == 0 { '★' } else { '·' });
118        }
119        Self { positions, chars }
120    }
121
122    /// Hexagonal close-pack: all hex centers within radius.
123    pub fn hex_cluster(radius: f32) -> Self {
124        let mut positions = Vec::new();
125        let mut chars = Vec::new();
126        let spacing = 1.0f32;
127        let h = spacing * 3.0f32.sqrt() * 0.5;
128        let imax = (radius / spacing) as i32 + 2;
129        for row in -imax..=imax {
130            for col in -imax..=imax {
131                let x = col as f32 * spacing + (row % 2) as f32 * spacing * 0.5;
132                let y = row as f32 * h;
133                if (x * x + y * y).sqrt() <= radius {
134                    positions.push(Vec3::new(x, y, 0.0));
135                    chars.push(if x.abs() < 0.01 && y.abs() < 0.01 { '⊕' } else { '◆' });
136                }
137            }
138        }
139        Self { positions, chars }
140    }
141
142    /// Archimedean spiral: r = a * θ.
143    pub fn spiral(turns: f32, density: usize) -> Self {
144        let mut positions = Vec::new();
145        let mut chars = Vec::new();
146        let n = (turns * density as f32) as usize;
147        let spiral_chars = ['·', '·', '✦', '·', '·', '+'];
148        for i in 0..n {
149            let t = i as f32 / density as f32;
150            let angle = t * TAU;
151            let r = t * 0.6;
152            positions.push(Vec3::new(angle.cos() * r, angle.sin() * r, 0.0));
153            chars.push(spiral_chars[i % spiral_chars.len()]);
154        }
155        Self { positions, chars }
156    }
157
158    /// Fibonacci (golden-ratio) phyllotaxis spiral — N points.
159    pub fn fibonacci_spiral(n: usize, scale: f32) -> Self {
160        let phi = (1.0 + 5.0f32.sqrt()) * 0.5;
161        let golden_angle = TAU / (phi * phi);
162        let mut positions = Vec::new();
163        let mut chars = Vec::new();
164        let fib_chars = ['·', '+', '✦', '·', '*', '◆'];
165        for i in 0..n {
166            let r = (i as f32).sqrt() * scale;
167            let angle = i as f32 * golden_angle;
168            positions.push(Vec3::new(angle.cos() * r, angle.sin() * r, 0.0));
169            chars.push(fib_chars[i % fib_chars.len()]);
170        }
171        Self { positions, chars }
172    }
173
174    /// Double helix (DNA-like): two interleaved spirals winding vertically.
175    pub fn dna_helix(height: f32, turns: f32, n: usize) -> Self {
176        let mut positions = Vec::new();
177        let mut chars = Vec::new();
178        let radius = 1.5f32;
179        for i in 0..n {
180            let t = i as f32 / n as f32;
181            let angle = t * turns * TAU;
182            let y = (t - 0.5) * height;
183            // Strand A
184            positions.push(Vec3::new(angle.cos() * radius, y, angle.sin() * radius));
185            chars.push('◆');
186            // Strand B (opposite phase)
187            let angle_b = angle + PI;
188            positions.push(Vec3::new(angle_b.cos() * radius, y, angle_b.sin() * radius));
189            chars.push('◇');
190            // Rung every 4 glyphs
191            if i % 4 == 0 {
192                let mid_x = (angle.cos() + angle_b.cos()) * radius * 0.5;
193                let mid_z = (angle.sin() + angle_b.sin()) * radius * 0.5;
194                positions.push(Vec3::new(mid_x, y, mid_z));
195                chars.push('═');
196            }
197        }
198        Self { positions, chars }
199    }
200
201    /// Triangle (equilateral) of side length `size`.
202    pub fn triangle(size: f32) -> Self {
203        let mut positions = Vec::new();
204        let mut chars = Vec::new();
205        let n = (size as usize).max(2);
206        // Three vertices
207        let verts = [
208            Vec3::new(0.0, size * 0.577, 0.0),
209            Vec3::new(-size * 0.5, -size * 0.289, 0.0),
210            Vec3::new( size * 0.5, -size * 0.289, 0.0),
211        ];
212        for side in 0..3 {
213            let a = verts[side];
214            let b = verts[(side + 1) % 3];
215            for i in 0..n {
216                let t = i as f32 / n as f32;
217                positions.push(a + (b - a) * t);
218                chars.push(if i == 0 { '▲' } else { '△' });
219            }
220        }
221        Self { positions, chars }
222    }
223
224    /// Lissajous figure: parametric (sin(a·t+δ), sin(b·t)) for n points.
225    pub fn lissajous(n: usize, a: f32, b: f32, delta: f32, scale: f32) -> Self {
226        let mut positions = Vec::new();
227        let mut chars = Vec::new();
228        let liss_chars = ['·', '✦', '+', '·', '*'];
229        for i in 0..n {
230            let t = i as f32 / n as f32 * TAU;
231            let x = (a * t + delta).sin() * scale;
232            let y = (b * t).sin() * scale;
233            positions.push(Vec3::new(x, y, 0.0));
234            chars.push(liss_chars[i % liss_chars.len()]);
235        }
236        Self { positions, chars }
237    }
238
239    /// Lorenz attractor sample: step the attractor n times and record positions.
240    pub fn lorenz_trace(n: usize, scale: f32) -> Self {
241        use crate::math::attractors::{step, initial_state, AttractorType};
242        let mut state = initial_state(AttractorType::Lorenz);
243        let mut positions = Vec::new();
244        let mut chars = Vec::new();
245        let trace_chars = ['·', '·', '✦', '·', '·', '*', '·'];
246        // Warm up
247        for _ in 0..500 {
248            let (next, _) = step(AttractorType::Lorenz, state, 0.01);
249            state = next;
250        }
251        for i in 0..n {
252            let (next, _) = step(AttractorType::Lorenz, state, 0.01);
253            state = next;
254            positions.push(state * scale);
255            chars.push(trace_chars[i % trace_chars.len()]);
256        }
257        Self { positions, chars }
258    }
259
260    /// Arrow pointing in a direction (normalized), length glyphs long.
261    pub fn arrow(length: usize, direction: Vec3) -> Self {
262        let dir = direction.normalize_or_zero();
263        let right = if dir.abs().dot(Vec3::X) < 0.9 {
264            dir.cross(Vec3::X).normalize()
265        } else {
266            dir.cross(Vec3::Y).normalize()
267        };
268        let mut positions = Vec::new();
269        let mut chars = Vec::new();
270        for i in 0..length {
271            positions.push(dir * i as f32);
272            chars.push('·');
273        }
274        // Arrowhead (3 glyphs)
275        let tip = dir * length as f32;
276        positions.push(tip);
277        chars.push('►');
278        positions.push(tip - dir * 0.8 + right * 0.5);
279        chars.push('╱');
280        positions.push(tip - dir * 0.8 - right * 0.5);
281        chars.push('╲');
282        Self { positions, chars }
283    }
284
285    /// Random scatter within radius, seeded for reproducibility.
286    pub fn scatter(n: usize, radius: f32, seed: u64) -> Self {
287        let mut positions = Vec::new();
288        let mut chars = Vec::new();
289        let scatter_chars = ['·', '+', '✦', '*', '◆', '◇'];
290        let mut rng = seed;
291        for i in 0..n {
292            rng = rng.wrapping_mul(0x6c62272e07bb0142).wrapping_add(0x62b821756295c58d);
293            let x = ((rng >> 32) as f32 / u32::MAX as f32) * 2.0 - 1.0;
294            rng = rng.wrapping_mul(0x6c62272e07bb0142).wrapping_add(0x62b821756295c58d);
295            let y = ((rng >> 32) as f32 / u32::MAX as f32) * 2.0 - 1.0;
296            rng = rng.wrapping_mul(0x6c62272e07bb0142).wrapping_add(0x62b821756295c58d);
297            let z = ((rng >> 32) as f32 / u32::MAX as f32) * 2.0 - 1.0;
298            let v = Vec3::new(x, y, z).normalize_or_zero();
299            rng = rng.wrapping_mul(0x6c62272e07bb0142).wrapping_add(0x62b821756295c58d);
300            let r = ((rng >> 32) as f32 / u32::MAX as f32).sqrt() * radius;
301            positions.push(v * r);
302            chars.push(scatter_chars[i % scatter_chars.len()]);
303        }
304        Self { positions, chars }
305    }
306
307    // ── Rune formations ──────────────────────────────────────────────────────
308
309    /// Rune: Sigma / S-curve.
310    pub fn rune_sigma() -> Self {
311        let pts: &[(f32, f32, char)] = &[
312            (1.5,  1.0, '╗'), (0.5,  1.0, '═'), (-0.5,  1.0, '═'), (-1.5,  1.0, '╔'),
313            (1.5,  0.0, '·'), (-1.5,  0.0, '·'),
314            (1.5, -1.0, '╝'), (0.5, -1.0, '═'), (-0.5, -1.0, '═'), (-1.5, -1.0, '╚'),
315            (0.0,  0.5, '╲'), (0.0, -0.5, '╲'),
316        ];
317        let positions = pts.iter().map(|&(x, y, _)| Vec3::new(x, y, 0.0)).collect();
318        let chars = pts.iter().map(|&(_, _, c)| c).collect();
319        Self { positions, chars }
320    }
321
322    /// Rune: Infinity / ∞ shape.
323    pub fn rune_infinity() -> Self {
324        let n = 32usize;
325        let mut positions = Vec::new();
326        let mut chars = Vec::new();
327        let inf_chars = ['·', '◆', '·', '✦'];
328        for i in 0..n {
329            let t = i as f32 / n as f32 * TAU;
330            // Lemniscate of Bernoulli
331            let denom = 1.0 + (t.sin()).powi(2);
332            let x = 2.5 * t.cos() / denom;
333            let y = 2.5 * t.cos() * t.sin() / denom;
334            positions.push(Vec3::new(x, y, 0.0));
335            chars.push(inf_chars[i % inf_chars.len()]);
336        }
337        Self { positions, chars }
338    }
339
340    /// Rune: Chaos — a 3-armed triskelion.
341    pub fn rune_chaos() -> Self {
342        let mut positions = Vec::new();
343        let mut chars = Vec::new();
344        let arms = 3;
345        let arm_len = 8;
346        for arm in 0..arms {
347            let base_angle = arm as f32 / arms as f32 * TAU;
348            for j in 0..arm_len {
349                let t = j as f32 / arm_len as f32;
350                let angle = base_angle + t * PI * 0.5;
351                let r = t * 2.5;
352                positions.push(Vec3::new(angle.cos() * r, angle.sin() * r, 0.0));
353                chars.push(if j == arm_len - 1 { '▲' } else { '·' });
354            }
355        }
356        // Center
357        positions.push(Vec3::ZERO);
358        chars.push('⊕');
359        Self { positions, chars }
360    }
361
362    // ── Boss formations ──────────────────────────────────────────────────────
363
364    /// Boss formation: Sierpinski triangle approximation (2 levels deep).
365    pub fn sierpinski(depth: u32, size: f32) -> Self {
366        let mut positions = Vec::new();
367        let mut chars = Vec::new();
368        sierpinski_recurse(&mut positions, &mut chars,
369            Vec3::new(0.0, size * 0.577, 0.0),
370            Vec3::new(-size * 0.5, -size * 0.289, 0.0),
371            Vec3::new( size * 0.5, -size * 0.289, 0.0),
372            depth);
373        Self { positions, chars }
374    }
375
376    /// Boss formation: Mandala (multiple concentric rings with rotational symmetry).
377    pub fn mandala(layers: usize, base_n: usize, base_r: f32) -> Self {
378        let mandala_chars = ['◆', '◇', '◈', '✦', '★', '·'];
379        let mut positions = Vec::new();
380        let mut chars = Vec::new();
381        positions.push(Vec3::ZERO);
382        chars.push('⊙');
383        for layer in 0..layers {
384            let n = base_n + layer * base_n;
385            let r = base_r * (layer + 1) as f32;
386            let phase_offset = if layer % 2 == 0 { 0.0 } else { PI / n as f32 };
387            let ch = mandala_chars[layer.min(mandala_chars.len() - 1)];
388            for i in 0..n {
389                let angle = i as f32 / n as f32 * TAU + phase_offset;
390                positions.push(Vec3::new(angle.cos() * r, angle.sin() * r, 0.0));
391                chars.push(ch);
392            }
393        }
394        Self { positions, chars }
395    }
396
397    // ── Combinators ───────────────────────────────────────────────────────────
398
399    /// Scale all positions by a scalar.
400    pub fn scaled(mut self, s: f32) -> Self {
401        for p in &mut self.positions { *p *= s; }
402        self
403    }
404
405    /// Rotate the entire formation around the Z axis by `angle` radians.
406    pub fn rotated_z(mut self, angle: f32) -> Self {
407        let (s, c) = angle.sin_cos();
408        for p in &mut self.positions {
409            let x = p.x * c - p.y * s;
410            let y = p.x * s + p.y * c;
411            p.x = x;
412            p.y = y;
413        }
414        self
415    }
416
417    /// Translate all positions by an offset.
418    pub fn translated(mut self, offset: Vec3) -> Self {
419        for p in &mut self.positions { *p += offset; }
420        self
421    }
422
423    /// Reflect across the Y axis (mirror left-right).
424    pub fn mirrored_x(mut self) -> Self {
425        for p in &mut self.positions { p.x = -p.x; }
426        self
427    }
428
429    /// Combine two formations into one.
430    pub fn join(mut self, other: Formation) -> Self {
431        self.positions.extend(other.positions);
432        self.chars.extend(other.chars);
433        self
434    }
435
436    /// Replace all characters with a single glyph.
437    pub fn with_char(mut self, ch: char) -> Self {
438        self.chars.iter_mut().for_each(|c| *c = ch);
439        self
440    }
441
442    /// Number of glyphs.
443    pub fn len(&self) -> usize { self.positions.len() }
444
445    /// True if empty.
446    pub fn is_empty(&self) -> bool { self.positions.is_empty() }
447
448    /// Compute the centroid of all positions.
449    pub fn centroid(&self) -> Vec3 {
450        if self.positions.is_empty() { return Vec3::ZERO; }
451        self.positions.iter().copied().sum::<Vec3>() / self.positions.len() as f32
452    }
453
454    /// Compute bounding radius (max distance from centroid).
455    pub fn bounding_radius(&self) -> f32 {
456        let c = self.centroid();
457        self.positions.iter().map(|p| (*p - c).length()).fold(0.0f32, f32::max)
458    }
459
460    /// Normalize so bounding_radius == 1.0.
461    pub fn normalized(self) -> Self {
462        let r = self.bounding_radius().max(0.001);
463        self.scaled(1.0 / r)
464    }
465}
466
467// ── Helpers ───────────────────────────────────────────────────────────────────
468
469fn sierpinski_recurse(
470    positions: &mut Vec<Vec3>,
471    chars: &mut Vec<char>,
472    a: Vec3, b: Vec3, c: Vec3,
473    depth: u32,
474) {
475    if depth == 0 {
476        positions.push(a);
477        positions.push(b);
478        positions.push(c);
479        chars.push('▲');
480        chars.push('▲');
481        chars.push('▲');
482        return;
483    }
484    let ab = (a + b) * 0.5;
485    let bc = (b + c) * 0.5;
486    let ca = (c + a) * 0.5;
487    sierpinski_recurse(positions, chars, a, ab, ca, depth - 1);
488    sierpinski_recurse(positions, chars, ab, b, bc, depth - 1);
489    sierpinski_recurse(positions, chars, ca, bc, c, depth - 1);
490}
491
492// Fix: The cross formation uses numeric literals for dx/dy, not negation operators
493// that could conflict with the unary minus. Redefine as a proper constant array.
494impl Formation {
495    /// Cross formation (reimplemented cleanly without operator conflicts).
496    #[allow(dead_code)]
497    fn cross_inner(arm_length: i32) -> Vec<(i32, i32, char)> {
498        let mut pts = vec![(0, 0, '╬')];
499        for i in 1..=arm_length {
500            pts.push(( i,  0, '═'));
501            pts.push((-i,  0, '═'));
502            pts.push(( 0,  i, '║'));
503            pts.push(( 0, -i, '║'));
504        }
505        pts
506    }
507}