#[derive(Debug, Clone)]
pub struct DecryptedTextConfig {
pub speed_ms: u64,
pub max_iterations: usize,
pub sequential: bool,
pub reveal_direction: RevealDirection,
pub use_original_chars_only: bool,
pub characters: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevealDirection {
Start, End, Center, }
impl Default for DecryptedTextConfig {
fn default() -> Self {
Self {
speed_ms: 50,
max_iterations: 10,
sequential: false,
reveal_direction: RevealDirection::Start,
use_original_chars_only: false,
characters: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+"
.to_string(),
}
}
}
#[derive(Debug)]
pub struct DecryptedTextState {
original_text: Vec<char>,
current_text: Vec<char>,
revealed_indices: Vec<bool>,
iteration: usize,
is_complete: bool,
}
impl DecryptedTextState {
pub fn new(text: &str) -> Self {
let original_text: Vec<char> = text.chars().collect();
let current_text = original_text.clone();
let revealed_indices = vec![false; original_text.len()];
Self {
original_text,
current_text,
revealed_indices,
iteration: 0,
is_complete: false,
}
}
pub fn update(&mut self, config: &DecryptedTextConfig) -> Vec<char> {
if self.is_complete {
return self.current_text.clone();
}
if config.sequential {
let revealed_count = self.revealed_indices.iter().filter(|&&r| r).count();
if revealed_count < self.original_text.len() {
let next_index = self.get_next_index(revealed_count, config.reveal_direction);
if next_index < self.original_text.len() {
self.revealed_indices[next_index] = true;
}
self.current_text = self.shuffle_text(config);
} else {
self.is_complete = true;
self.current_text = self.original_text.clone();
}
} else {
self.iteration += 1;
if self.iteration >= config.max_iterations {
self.is_complete = true;
self.current_text = self.original_text.clone();
} else {
self.current_text = self.shuffle_text(config);
}
}
self.current_text.clone()
}
fn get_next_index(&self, revealed_count: usize, direction: RevealDirection) -> usize {
let text_len = self.original_text.len();
match direction {
RevealDirection::Start => revealed_count,
RevealDirection::End => {
if revealed_count < text_len {
text_len - 1 - revealed_count
} else {
0
}
}
RevealDirection::Center => {
let middle = text_len / 2;
let offset = revealed_count / 2;
let next_index = if revealed_count.is_multiple_of(2) {
middle + offset
} else {
middle.saturating_sub(offset + 1)
};
if next_index < text_len && !self.revealed_indices[next_index] {
next_index
} else {
self.revealed_indices
.iter()
.position(|&r| !r)
.unwrap_or(0)
}
}
}
}
fn shuffle_text(&self, config: &DecryptedTextConfig) -> Vec<char> {
if config.use_original_chars_only {
let available_chars: Vec<char> = self
.original_text
.iter()
.filter(|&&c| c != ' ')
.copied()
.collect();
let mut shuffled_chars = available_chars.clone();
for i in (1..shuffled_chars.len()).rev() {
let j = fastrand::usize(0..=i);
shuffled_chars.swap(i, j);
}
let mut char_index = 0;
self.original_text
.iter()
.enumerate()
.map(|(i, &c)| {
if c == ' ' {
' '
} else if config.sequential && self.revealed_indices[i] {
self.original_text[i]
} else if char_index < shuffled_chars.len() {
let result = shuffled_chars[char_index];
char_index += 1;
result
} else {
c
}
})
.collect()
} else {
let available_chars: Vec<char> = config.characters.chars().collect();
self.original_text
.iter()
.enumerate()
.map(|(i, &c)| {
if c == ' ' {
' '
} else if config.sequential && self.revealed_indices[i] {
self.original_text[i]
} else if !available_chars.is_empty() {
available_chars[fastrand::usize(0..available_chars.len())]
} else {
c
}
})
.collect()
}
}
pub fn is_complete(&self) -> bool {
self.is_complete
}
pub fn revealed_indices(&self) -> &[bool] {
&self.revealed_indices
}
pub fn reset(&mut self) {
self.current_text = self.original_text.clone();
self.revealed_indices.fill(false);
self.iteration = 0;
self.is_complete = false;
}
pub fn display_text(&self) -> String {
self.current_text.iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sequential_start() {
let config = DecryptedTextConfig {
sequential: true,
reveal_direction: RevealDirection::Start,
..Default::default()
};
let mut state = DecryptedTextState::new("HELLO");
state.update(&config);
assert!(state.revealed_indices[0]);
assert!(!state.revealed_indices[1]);
state.update(&config);
assert!(state.revealed_indices[1]);
state.update(&config);
state.update(&config);
state.update(&config);
state.update(&config);
assert!(state.is_complete());
assert_eq!(state.display_text(), "HELLO");
}
#[test]
fn test_sequential_end() {
let config = DecryptedTextConfig {
sequential: true,
reveal_direction: RevealDirection::End,
..Default::default()
};
let mut state = DecryptedTextState::new("HELLO");
state.update(&config);
assert!(state.revealed_indices[4]);
assert!(!state.revealed_indices[3]);
state.update(&config);
assert!(state.revealed_indices[3]);
}
#[test]
fn test_sequential_center() {
let config = DecryptedTextConfig {
sequential: true,
reveal_direction: RevealDirection::Center,
..Default::default()
};
let mut state = DecryptedTextState::new("HELLO");
state.update(&config);
assert!(state.revealed_indices[2]);
state.update(&config);
assert!(state.revealed_indices[1]); }
#[test]
fn test_random_scramble() {
let config = DecryptedTextConfig {
sequential: false,
max_iterations: 5,
..Default::default()
};
let mut state = DecryptedTextState::new("TEST");
for _ in 0..4 {
let text = state.update(&config);
assert_eq!(text.len(), 4);
assert!(!state.is_complete());
}
let final_text = state.update(&config);
assert!(state.is_complete());
assert_eq!(final_text.iter().collect::<String>(), "TEST");
}
#[test]
fn test_spaces_preserved() {
let config = DecryptedTextConfig::default();
let mut state = DecryptedTextState::new("HI WORLD");
let text = state.update(&config);
assert_eq!(text[2], ' ');
}
#[test]
fn test_reset() {
let config = DecryptedTextConfig {
sequential: true,
..Default::default()
};
let mut state = DecryptedTextState::new("TEST");
state.update(&config);
state.update(&config);
assert!(state.revealed_indices[0]);
state.reset();
assert!(!state.revealed_indices[0]);
assert_eq!(state.iteration, 0);
assert!(!state.is_complete());
}
}