i_ching/core/
divination.rs1use crate::core::reading::{Age, Line, Polarity, Reading};
2use rand::Rng;
3
4pub struct Diviner {
5 rng: rand::rngs::ThreadRng,
6}
7
8impl Diviner {
9 pub fn new() -> Self {
10 Self {
11 rng: rand::rng(),
12 }
13 }
14
15 pub fn cast_reading(&mut self, question: Option<String>) -> Reading {
17 let lines = [
18 self.cast_line(),
19 self.cast_line(),
20 self.cast_line(),
21 self.cast_line(),
22 self.cast_line(),
23 self.cast_line(),
24 ];
25
26 Reading::new(lines, question)
27 }
28
29 fn number_to_line(number: u8) -> Line {
37 match number {
38 6 => Line::new(Age::Old, Polarity::Yin), 7 => Line::new(Age::Young, Polarity::Yang), 8 => Line::new(Age::Young, Polarity::Yin), 9 => Line::new(Age::Old, Polarity::Yang), _ => panic!("Invalid line number: {}. Must be 6, 7, 8, or 9", number),
43 }
44 }
45
46 fn cast_line(&mut self) -> Line {
51 let coin_sum: u8 = (0..3)
52 .map(|_| if self.rng.random_bool(0.5) { 3 } else { 2 })
53 .sum();
54
55 Self::number_to_line(coin_sum)
56 }
57
58 pub fn cast_reading_from_numbers(
60 &self,
61 numbers: [u8; 6],
62 question: Option<String>,
63 ) -> Result<Reading, anyhow::Error> {
64 let mut lines = [Line::new(Age::Young, Polarity::Yang); 6];
65
66 for (i, &num) in numbers.iter().enumerate() {
67 lines[i] = Line::from_traditional_number(num)?;
68 }
69
70 Ok(Reading::new(lines, question))
71 }
72
73 #[cfg(test)]
75 pub fn cast_reading_with_numbers(
76 &mut self,
77 numbers: [u8; 6],
78 question: Option<String>,
79 ) -> Reading {
80 let lines = numbers.map(Self::number_to_line);
81 Reading::new(lines, question)
82 }
83}
84
85impl Default for Diviner {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn test_line_number_interpretation() {
97 let mut diviner = Diviner::new();
98
99 let numbers = [6, 7, 8, 9, 6, 8];
101
102 let reading = diviner.cast_reading_with_numbers(numbers, Some("Test question".to_string()));
103
104 assert_eq!(reading.traditional_numbers(), [6, 7, 8, 9, 6, 8]);
105 assert!(reading.has_changing_lines());
106 assert_eq!(reading.changing_line_positions(), vec![1, 4, 5]); }
108
109 #[test]
110 fn test_cast_from_numbers() {
111 let diviner = Diviner::new();
112 let numbers = [7, 8, 9, 6, 7, 8];
113
114 let reading = diviner
115 .cast_reading_from_numbers(numbers, Some("Test from numbers".to_string()))
116 .unwrap();
117
118 assert_eq!(reading.traditional_numbers(), numbers);
119 assert!(reading.has_changing_lines());
120 assert_eq!(reading.changing_line_positions(), vec![3, 4]); }
122
123 #[test]
124 fn test_invalid_numbers() {
125 let diviner = Diviner::new();
126 let invalid_numbers = [7, 8, 5, 6, 7, 8]; let result = diviner.cast_reading_from_numbers(invalid_numbers, None);
129 assert!(result.is_err());
130 assert!(result
131 .unwrap_err()
132 .to_string()
133 .contains("Invalid line number: 5"));
134 }
135
136 #[test]
137 fn test_random_casting() {
138 let mut diviner = Diviner::new();
139
140 for _ in 0..10 {
142 let reading = diviner.cast_reading(Some("Random test".to_string()));
143
144 for &num in &reading.traditional_numbers() {
146 assert!([6, 7, 8, 9].contains(&num));
147 }
148
149 let hexagram = reading.primary_hexagram();
151 assert!(hexagram >= 1 && hexagram <= 64);
152 }
153 }
154}