Skip to main content

blizz_ui/
decode.rs

1use rand::{Rng, RngExt};
2
3const GARBLE_CHARS: &[u8] =
4  b"!@#$%^&*()_+-=[]{}|;:,.<>?`~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
5
6pub fn garble_text<R: Rng>(length: usize, rng: &mut R) -> String {
7  (0..length)
8    .map(|_| GARBLE_CHARS[rng.random_range(0..GARBLE_CHARS.len())] as char)
9    .collect()
10}
11
12pub fn garble_frame<R: Rng>(from_len: usize, to_len: usize, progress: f64, rng: &mut R) -> String {
13  let interpolated = from_len as f64 + (to_len as f64 - from_len as f64) * progress.clamp(0.0, 1.0);
14  let length = interpolated.round().max(1.0) as usize;
15
16  garble_text(length, rng)
17}
18
19pub fn decode_frame<R: Rng>(target: &str, revealed_count: usize, rng: &mut R) -> String {
20  let chars: Vec<char> = target.chars().collect();
21  let revealed = revealed_count.min(chars.len());
22  let prefix: String = chars[..revealed].iter().collect();
23  let remaining = chars.len().saturating_sub(revealed);
24
25  if remaining == 0 {
26    return prefix;
27  }
28
29  let suffix = garble_text(remaining, rng);
30  format!("{prefix}{suffix}")
31}
32
33pub fn encode_frame<R: Rng>(target: &str, garbled_count: usize, rng: &mut R) -> String {
34  let chars: Vec<char> = target.chars().collect();
35  let garbled = garbled_count.min(chars.len());
36  let suffix: String = chars[garbled..].iter().collect();
37
38  if garbled == 0 {
39    return suffix;
40  }
41
42  let prefix = garble_text(garbled, rng);
43  format!("{prefix}{suffix}")
44}
45
46pub fn is_fully_revealed(target: &str, revealed_count: usize) -> bool {
47  revealed_count >= target.chars().count()
48}
49
50pub fn is_fully_garbled(target: &str, garbled_count: usize) -> bool {
51  garbled_count >= target.chars().count()
52}
53
54/// Combines garble-grow and decode-reveal into a single call.
55/// For ticks 0..garble_ticks, grows garbled text from 1 to target length.
56/// For ticks >= garble_ticks, progressively reveals the target text.
57pub fn decode_text<R: Rng>(target: &str, tick: usize, garble_ticks: usize, rng: &mut R) -> String {
58  if tick < garble_ticks {
59    garble_frame(1, target.len(), tick as f64 / garble_ticks as f64, rng)
60  } else {
61    decode_frame(target, tick - garble_ticks, rng)
62  }
63}
64
65#[cfg(test)]
66mod tests {
67  use super::*;
68  use rand::SeedableRng;
69  use rand::rngs::SmallRng;
70
71  fn test_rng() -> SmallRng {
72    SmallRng::seed_from_u64(42)
73  }
74
75  #[test]
76  fn garble_text_produces_correct_length() {
77    let mut rng = test_rng();
78
79    assert_eq!(garble_text(10, &mut rng).len(), 10);
80    assert_eq!(garble_text(0, &mut rng).len(), 0);
81    assert_eq!(garble_text(100, &mut rng).len(), 100);
82  }
83
84  #[test]
85  fn garble_text_uses_only_valid_chars() {
86    let mut rng = test_rng();
87    let text = garble_text(200, &mut rng);
88
89    for ch in text.chars() {
90      assert!(GARBLE_CHARS.contains(&(ch as u8)), "unexpected char: {ch}");
91    }
92  }
93
94  #[test]
95  fn garble_frame_interpolates_length() {
96    let mut rng = test_rng();
97
98    let start = garble_frame(5, 20, 0.0, &mut rng);
99    let middle = garble_frame(5, 20, 0.5, &mut rng);
100    let end = garble_frame(5, 20, 1.0, &mut rng);
101
102    assert_eq!(start.len(), 5);
103    assert_eq!(middle.len(), 13); // round(5 + 15*0.5) = 13
104    assert_eq!(end.len(), 20);
105  }
106
107  #[test]
108  fn garble_frame_clamps_progress() {
109    let mut rng = test_rng();
110
111    let below = garble_frame(5, 20, -1.0, &mut rng);
112    let above = garble_frame(5, 20, 2.0, &mut rng);
113
114    assert_eq!(below.len(), 5);
115    assert_eq!(above.len(), 20);
116  }
117
118  #[test]
119  fn decode_frame_reveals_prefix_progressively() {
120    let mut rng = test_rng();
121    let target = "hello world";
122
123    let frame0 = decode_frame(target, 0, &mut rng);
124    let frame5 = decode_frame(target, 5, &mut rng);
125    let frame_full = decode_frame(target, 11, &mut rng);
126
127    assert_eq!(frame0.chars().count(), target.chars().count());
128    assert!(!frame0.starts_with('h'));
129
130    assert!(frame5.starts_with("hello"));
131    assert_eq!(frame5.chars().count(), target.chars().count());
132
133    assert_eq!(frame_full, "hello world");
134  }
135
136  #[test]
137  fn decode_frame_handles_beyond_target_length() {
138    let mut rng = test_rng();
139    let target = "hi";
140
141    let frame = decode_frame(target, 100, &mut rng);
142
143    assert_eq!(frame, "hi");
144  }
145
146  #[test]
147  fn is_fully_revealed_checks_char_count() {
148    assert!(!is_fully_revealed("hello", 4));
149    assert!(is_fully_revealed("hello", 5));
150    assert!(is_fully_revealed("hello", 6));
151  }
152
153  #[test]
154  fn encode_frame_garbles_prefix_progressively() {
155    let mut rng = test_rng();
156    let target = "hello world";
157
158    let frame0 = encode_frame(target, 0, &mut rng);
159    let frame5 = encode_frame(target, 5, &mut rng);
160    let frame_full = encode_frame(target, 11, &mut rng);
161
162    assert_eq!(frame0, "hello world");
163
164    assert!(frame5.ends_with(" world"));
165    assert_eq!(frame5.chars().count(), target.chars().count());
166
167    assert_eq!(frame_full.chars().count(), target.chars().count());
168    assert!(!frame_full.contains("hello"));
169  }
170
171  #[test]
172  fn encode_frame_handles_beyond_target_length() {
173    let mut rng = test_rng();
174    let target = "hi";
175
176    let frame = encode_frame(target, 100, &mut rng);
177
178    assert_eq!(frame.chars().count(), 2);
179  }
180
181  #[test]
182  fn is_fully_garbled_checks_char_count() {
183    assert!(!is_fully_garbled("hello", 4));
184    assert!(is_fully_garbled("hello", 5));
185    assert!(is_fully_garbled("hello", 6));
186  }
187
188  #[test]
189  fn decode_preserves_multibyte_target() {
190    let mut rng = test_rng();
191    let target = "café";
192
193    let frame = decode_frame(target, 3, &mut rng);
194
195    assert!(frame.starts_with("caf"));
196    assert_eq!(frame.chars().count(), 4);
197  }
198
199  #[test]
200  fn decode_multibyte_char_count_stable_but_byte_len_varies() {
201    let mut rng = test_rng();
202    let target = "café résumé";
203    let target_chars = target.chars().count();
204
205    for revealed in 0..=target_chars {
206      let frame = decode_frame(target, revealed, &mut rng);
207      assert_eq!(
208        frame.chars().count(),
209        target_chars,
210        "char count must equal target at revealed={revealed}"
211      );
212    }
213
214    let partial = decode_frame(target, 2, &mut test_rng());
215    assert_ne!(
216      partial.len(),
217      target.len(),
218      "byte length of partially-decoded multi-byte string should differ from target — \
219       centering must use chars().count(), not len()"
220    );
221  }
222
223  #[test]
224  fn decode_text_garbles_then_reveals() {
225    let mut rng = test_rng();
226    let target = "hello";
227    let garble_ticks = 5;
228
229    let early = decode_text(target, 2, garble_ticks, &mut rng);
230    assert!(!early.is_empty());
231    assert_ne!(early, "hello");
232
233    let revealed = decode_text(target, garble_ticks + 10, garble_ticks, &mut rng);
234    assert_eq!(revealed, "hello");
235  }
236}