use rand::{Rng, RngExt};
use super::math::{self, CORE_MAX_RADIUS, CROSSFADE_TICKS};
pub use math::{DistanceMap, build_distance_map, distance_at};
const GARBLE_CHARS: &[u8] =
b"!@#$%^&*()_+-=[]{}|;:,.<>?`~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
pub const REVEAL_SPEED: f64 = 0.6;
pub const GARBLE_FRINGE: f64 = 6.0;
pub const DISSOLVE_TILE_COLS: usize = 6;
pub const DISSOLVE_TILE_ROWS: usize = 5;
const DISSOLVE_TICKS_PER_TILE: usize = 1;
const DISSOLVE_GARBLE_TICKS_MIN: usize = 4;
const DISSOLVE_GARBLE_TICKS_MAX: usize = 8;
const DISSOLVE_START_JITTER: usize = 4;
const DISSOLVE_BOUNDARY_NOISE: usize = 6;
const REVEAL_NOISE: f64 = 8.0;
const DISSOLVE_CHAR_NOISE_TICKS: usize = 2;
pub fn radial_reveal_frame<R: Rng>(
lines: &[&str],
map: &DistanceMap,
garble_radius: f64,
resolve_radius: f64,
rng: &mut R,
) -> Vec<String> {
lines
.iter()
.enumerate()
.map(|(row, line)| {
line
.chars()
.enumerate()
.map(|(col, ch)| radial_classify(ch, row, col, map, garble_radius, resolve_radius, rng))
.collect()
})
.collect()
}
fn radial_classify<R: Rng>(
ch: char,
row: usize,
col: usize,
map: &DistanceMap,
garble_radius: f64,
resolve_radius: f64,
rng: &mut R,
) -> char {
if ch == ' ' {
return ' ';
}
let noise = rng.random_range(-REVEAL_NOISE..REVEAL_NOISE);
let d = distance_at(map, row, col) + noise;
if d <= resolve_radius {
ch
} else if d <= garble_radius {
random_garble_char(rng)
} else {
' '
}
}
pub fn entrance_frame<R: Rng>(
lines: &[&str],
map: &DistanceMap,
tick: usize,
rng: &mut R,
) -> Vec<String> {
let core_end = math::core_total_ticks();
let reveal_start = core_end.saturating_sub(CROSSFADE_TICKS);
let in_core = tick < core_end;
let in_reveal = tick >= reveal_start;
lines
.iter()
.enumerate()
.map(|(row, line)| {
line
.chars()
.enumerate()
.map(|(col, ch)| {
let reveal_char = if in_reveal {
reveal_char_at(ch, row, col, tick, reveal_start, map, rng)
} else {
' '
};
if reveal_char != ' ' {
return reveal_char;
}
if in_core {
core_char_at(row, col, tick, rng)
} else {
' '
}
})
.collect()
})
.collect()
}
fn core_char_at<R: Rng>(row: usize, col: usize, tick: usize, rng: &mut R) -> char {
let radius = math::core_radius(tick);
let d = math::core_distance(row as f64, col as f64);
let noise = rng.random_range(-REVEAL_NOISE..REVEAL_NOISE);
if d + noise <= radius {
random_garble_char(rng)
} else {
' '
}
}
fn reveal_char_at<R: Rng>(
ch: char,
row: usize,
col: usize,
tick: usize,
reveal_start: usize,
map: &DistanceMap,
rng: &mut R,
) -> char {
if ch == ' ' {
return ' ';
}
let reveal_tick = tick - reveal_start;
let garble_radius = CORE_MAX_RADIUS + reveal_tick as f64 * REVEAL_SPEED;
let resolve_radius = garble_radius - GARBLE_FRINGE;
let noise = rng.random_range(-REVEAL_NOISE..REVEAL_NOISE);
let d = distance_at(map, row, col) + noise;
if d <= resolve_radius {
ch
} else if d <= garble_radius {
random_garble_char(rng)
} else {
' '
}
}
pub fn is_entrance_complete(map: &DistanceMap, tick: usize) -> bool {
let reveal_start = math::core_total_ticks().saturating_sub(CROSSFADE_TICKS);
if tick < reveal_start {
return false;
}
let reveal_tick = tick - reveal_start;
let garble_radius = CORE_MAX_RADIUS + reveal_tick as f64 * REVEAL_SPEED;
let resolve_radius = garble_radius - GARBLE_FRINGE;
resolve_radius >= map.max_distance
}
#[derive(Clone, Debug)]
pub struct DissolveState {
tile_assignments: Vec<Vec<usize>>,
tile_starts: Vec<usize>,
tile_garble_durations: Vec<usize>,
pub total_ticks: usize,
}
pub fn build_dissolve_state<R: Rng>(lines: &[&str], rng: &mut R) -> DissolveState {
let rows = lines.len();
let cols = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
let tile_cols = cols.div_ceil(DISSOLVE_TILE_COLS);
let tile_rows = rows.div_ceil(DISSOLVE_TILE_ROWS);
let tile_count = tile_cols * tile_rows;
let tile_assignments = build_noisy_tile_assignments(lines, tile_cols, tile_count, rng);
let tile_starts = build_tile_starts(tile_count, rng);
let tile_garble_durations = build_tile_garble_durations(tile_count, rng);
let total_ticks = tile_starts
.iter()
.zip(tile_garble_durations.iter())
.map(|(start, dur)| start + dur + DISSOLVE_CHAR_NOISE_TICKS)
.max()
.unwrap_or(0);
DissolveState {
tile_assignments,
tile_starts,
tile_garble_durations,
total_ticks,
}
}
fn build_noisy_tile_assignments<R: Rng>(
lines: &[&str],
tile_cols: usize,
tile_count: usize,
rng: &mut R,
) -> Vec<Vec<usize>> {
lines
.iter()
.enumerate()
.map(|(row, line)| {
line
.chars()
.enumerate()
.map(|(col, _)| {
let base = tile_index(row, col, tile_cols);
if !is_tile_boundary(row, col) {
return base;
}
let offset: i32 =
rng.random_range(-(DISSOLVE_BOUNDARY_NOISE as i32)..=(DISSOLVE_BOUNDARY_NOISE as i32));
(base as i32 + offset).clamp(0, tile_count as i32 - 1) as usize
})
.collect()
})
.collect()
}
fn is_tile_boundary(row: usize, col: usize) -> bool {
let row_pos = row % DISSOLVE_TILE_ROWS;
let col_pos = col % DISSOLVE_TILE_COLS;
row_pos == 0
|| row_pos == DISSOLVE_TILE_ROWS - 1
|| col_pos == 0
|| col_pos == DISSOLVE_TILE_COLS - 1
}
fn build_tile_starts<R: Rng>(tile_count: usize, rng: &mut R) -> Vec<usize> {
let mut order: Vec<usize> = (0..tile_count).collect();
shuffle(&mut order, rng);
let mut starts = vec![0usize; tile_count];
for (position, &tile_idx) in order.iter().enumerate() {
let base = position * DISSOLVE_TICKS_PER_TILE;
let jitter = rng.random_range(0..=DISSOLVE_START_JITTER);
starts[tile_idx] = base.saturating_sub(jitter);
}
starts
}
fn build_tile_garble_durations<R: Rng>(tile_count: usize, rng: &mut R) -> Vec<usize> {
(0..tile_count)
.map(|_| rng.random_range(DISSOLVE_GARBLE_TICKS_MIN..=DISSOLVE_GARBLE_TICKS_MAX))
.collect()
}
pub fn dissolve_frame<R: Rng>(
lines: &[&str],
state: &DissolveState,
tick: usize,
rng: &mut R,
) -> Vec<String> {
lines
.iter()
.enumerate()
.map(|(row, line)| {
line
.chars()
.enumerate()
.map(|(col, ch)| {
if ch == ' ' {
return ' ';
}
let tile_idx = state.tile_assignments[row][col];
let char_noise = rng.random_range(0..=DISSOLVE_CHAR_NOISE_TICKS);
let effective_tick = tick.saturating_sub(char_noise);
dissolve_char_from_state(ch, tile_idx, effective_tick, state, rng)
})
.collect()
})
.collect()
}
fn dissolve_char_from_state<R: Rng>(
ch: char,
tile_idx: usize,
tick: usize,
state: &DissolveState,
rng: &mut R,
) -> char {
let start = state.tile_starts[tile_idx];
if tick < start {
return ch;
}
let elapsed = tick - start;
let garble_duration = state.tile_garble_durations[tile_idx];
if elapsed < garble_duration {
random_garble_char(rng)
} else {
' '
}
}
fn tile_index(row: usize, col: usize, tile_cols: usize) -> usize {
let tr = row / DISSOLVE_TILE_ROWS;
let tc = col / DISSOLVE_TILE_COLS;
tr * tile_cols + tc
}
fn random_garble_char<R: Rng>(rng: &mut R) -> char {
GARBLE_CHARS[rng.random_range(0..GARBLE_CHARS.len())] as char
}
fn shuffle<R: Rng>(slice: &mut [usize], rng: &mut R) {
for i in (1..slice.len()).rev() {
let j = rng.random_range(0..=i);
slice.swap(i, j);
}
}
#[cfg(test)]
mod tests {
use super::math::{
ANCHOR_COL, ANCHOR_ROW, CORE_ROW_OFFSET, CROSSFADE_TICKS, core_grow_ticks, core_total_ticks,
};
use super::*;
fn sample_lines() -> Vec<&'static str> {
vec![" ▓▓▓ ", " ▓▓▓▓▓ ", "▓▓▓▓▓▓▓", " ▓▓▓▓▓ ", " ▓▓▓ "]
}
#[test]
fn radial_reveal_shows_nothing_at_zero_radius() {
let lines = sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let frame = radial_reveal_frame(&lines, &map, 0.0, -6.0, &mut rng);
for line in &frame {
assert!(line.chars().all(|c| c == ' '));
}
}
#[test]
fn radial_reveal_shows_all_at_large_radius() {
let lines = sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let frame = radial_reveal_frame(&lines, &map, 100.0, 100.0, &mut rng);
assert_eq!(frame[2], lines[2]);
}
#[test]
fn dissolve_frame_preserves_all_at_tick_zero() {
let lines = sample_lines();
let mut rng = rand::rng();
let state = build_dissolve_state(&lines, &mut rng);
let frame = dissolve_frame(&lines, &state, 0, &mut rng);
for (original, result) in lines.iter().zip(frame.iter()) {
for (oc, rc) in original.chars().zip(result.chars()) {
if oc == ' ' {
assert_eq!(rc, ' ');
} else {
assert_ne!(rc, ' ');
}
}
}
}
#[test]
fn dissolve_frame_clears_all_after_completion() {
let lines = sample_lines();
let mut rng = rand::rng();
let state = build_dissolve_state(&lines, &mut rng);
let frame = dissolve_frame(&lines, &state, state.total_ticks + 50, &mut rng);
for line in &frame {
assert!(line.chars().all(|c| c == ' '));
}
}
#[test]
fn dissolve_state_has_valid_total_ticks() {
let lines = sample_lines();
let mut rng = rand::rng();
let state = build_dissolve_state(&lines, &mut rng);
assert!(state.total_ticks > 0);
}
#[test]
fn dissolve_state_tile_assignments_cover_grid() {
let lines = sample_lines();
let mut rng = rand::rng();
let state = build_dissolve_state(&lines, &mut rng);
assert_eq!(state.tile_assignments.len(), lines.len());
for (row_assignments, line) in state.tile_assignments.iter().zip(lines.iter()) {
assert_eq!(row_assignments.len(), line.chars().count());
}
}
#[test]
fn entrance_frame_shows_content_during_reveal() {
let lines = sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let reveal_start = core_total_ticks().saturating_sub(CROSSFADE_TICKS);
let tick = reveal_start + 200;
let frame = entrance_frame(&lines, &map, tick, &mut rng);
let has_non_space = frame.iter().any(|l| l.chars().any(|c| c != ' '));
assert!(has_non_space);
}
#[test]
fn entrance_frame_resolves_fully_at_large_tick() {
let lines = sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let frame = entrance_frame(&lines, &map, 5000, &mut rng);
assert_eq!(frame[2], lines[2]);
}
#[test]
fn is_entrance_complete_false_at_start() {
let lines = sample_lines();
let map = build_distance_map(&lines);
assert!(!is_entrance_complete(&map, 0));
}
#[test]
fn is_entrance_complete_true_at_large_tick() {
let lines = sample_lines();
let map = build_distance_map(&lines);
assert!(is_entrance_complete(&map, 5000));
}
#[test]
fn dissolve_frame_garbles_midway() {
let lines = sample_lines();
let mut rng = rand::rng();
let state = build_dissolve_state(&lines, &mut rng);
let mid = state.total_ticks / 2;
let frame = dissolve_frame(&lines, &state, mid, &mut rng);
let has_garble = frame.iter().any(|l| {
l.chars()
.any(|c| c != ' ' && !lines.iter().any(|orig| orig.contains(c)))
});
assert!(has_garble);
}
#[test]
fn tile_index_maps_positions_to_tiles() {
assert_eq!(tile_index(0, 0, 3), 0);
assert_eq!(tile_index(0, DISSOLVE_TILE_COLS, 3), 1);
assert_eq!(tile_index(DISSOLVE_TILE_ROWS, 0, 3), 3);
}
#[test]
fn is_tile_boundary_detects_edges() {
assert!(is_tile_boundary(0, 0));
assert!(is_tile_boundary(DISSOLVE_TILE_ROWS - 1, 0));
assert!(is_tile_boundary(0, DISSOLVE_TILE_COLS - 1));
assert!(!is_tile_boundary(1, 1));
}
#[test]
fn shuffle_produces_permutation() {
let mut rng = rand::rng();
let mut data: Vec<usize> = (0..10).collect();
shuffle(&mut data, &mut rng);
let mut sorted = data.clone();
sorted.sort();
assert_eq!(sorted, (0..10).collect::<Vec<_>>());
}
#[test]
fn radial_reveal_garbles_in_fringe_zone() {
let lines = sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let garble_radius = map.max_distance + 10.0;
let resolve_radius = -100.0;
let frame = radial_reveal_frame(&lines, &map, garble_radius, resolve_radius, &mut rng);
let has_non_original = frame[2] != lines[2];
assert!(has_non_original);
}
fn large_sample_lines() -> Vec<&'static str> {
vec![
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
"▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓",
]
}
#[test]
fn entrance_frame_core_produces_garble_near_anchor() {
let lines = large_sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let tick = core_grow_ticks();
let frame = entrance_frame(&lines, &map, tick, &mut rng);
let core_row = (ANCHOR_ROW + CORE_ROW_OFFSET) as usize;
if core_row < frame.len() {
let has_garble = frame[core_row].chars().any(|c| c != ' ');
assert!(has_garble);
}
}
#[test]
fn entrance_frame_crossfade_blends_core_and_reveal() {
let lines = large_sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let reveal_start = core_total_ticks().saturating_sub(CROSSFADE_TICKS);
let tick = reveal_start + CROSSFADE_TICKS / 2;
let frame = entrance_frame(&lines, &map, tick, &mut rng);
let has_content = frame.iter().any(|l| l.chars().any(|c| c != ' '));
assert!(has_content);
}
#[test]
fn entrance_before_reveal_only_has_core() {
let lines = large_sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let reveal_start = core_total_ticks().saturating_sub(CROSSFADE_TICKS);
let tick = reveal_start.saturating_sub(1).max(1);
let frame = entrance_frame(&lines, &map, tick, &mut rng);
let has_content = frame.iter().any(|l| l.chars().any(|c| c != ' '));
assert!(has_content);
}
#[test]
fn build_dissolve_state_large_grid() {
let lines = large_sample_lines();
let mut rng = rand::rng();
let state = build_dissolve_state(&lines, &mut rng);
assert!(state.total_ticks > 0);
assert!(!state.tile_starts.is_empty());
assert!(!state.tile_garble_durations.is_empty());
}
#[test]
fn dissolve_frame_large_grid_at_various_ticks() {
let lines = large_sample_lines();
let mut rng = rand::rng();
let state = build_dissolve_state(&lines, &mut rng);
let early = dissolve_frame(&lines, &state, 1, &mut rng);
assert_eq!(early.len(), lines.len());
let late = dissolve_frame(&lines, &state, state.total_ticks + 100, &mut rng);
for line in &late {
assert!(line.chars().all(|c| c == ' '));
}
}
#[test]
fn random_garble_char_produces_valid_chars() {
let mut rng = rand::rng();
for _ in 0..100 {
let ch = random_garble_char(&mut rng);
assert!(GARBLE_CHARS.contains(&(ch as u8)));
}
}
#[test]
fn build_tile_starts_covers_all_tiles() {
let mut rng = rand::rng();
let starts = build_tile_starts(10, &mut rng);
assert_eq!(starts.len(), 10);
}
#[test]
fn build_tile_garble_durations_in_range() {
let mut rng = rand::rng();
let durations = build_tile_garble_durations(10, &mut rng);
assert_eq!(durations.len(), 10);
for d in &durations {
assert!(*d >= DISSOLVE_GARBLE_TICKS_MIN);
assert!(*d <= DISSOLVE_GARBLE_TICKS_MAX);
}
}
#[test]
fn build_noisy_tile_assignments_has_boundary_noise() {
let lines = large_sample_lines();
let mut rng = rand::rng();
let tile_cols = lines[0].chars().count().div_ceil(DISSOLVE_TILE_COLS);
let tile_rows = lines.len().div_ceil(DISSOLVE_TILE_ROWS);
let tile_count = tile_cols * tile_rows;
let assignments = build_noisy_tile_assignments(&lines, tile_cols, tile_count, &mut rng);
assert_eq!(assignments.len(), lines.len());
assert_eq!(assignments[0].len(), lines[0].chars().count());
}
#[test]
fn dissolve_char_from_state_before_start() {
let mut rng = rand::rng();
let lines = large_sample_lines();
let state = build_dissolve_state(&lines, &mut rng);
let max_start = *state.tile_starts.iter().max().unwrap();
let tile_with_max = state
.tile_starts
.iter()
.position(|&s| s == max_start)
.unwrap();
let before = dissolve_char_from_state('▓', tile_with_max, 0, &state, &mut rng);
assert_eq!(before, '▓');
}
#[test]
fn dissolve_char_from_state_during_garble() {
let mut rng = rand::rng();
let lines = large_sample_lines();
let state = build_dissolve_state(&lines, &mut rng);
let start = state.tile_starts[0];
let during = dissolve_char_from_state('▓', 0, start + 1, &state, &mut rng);
assert!(GARBLE_CHARS.contains(&(during as u8)));
}
#[test]
fn dissolve_char_from_state_after_garble() {
let mut rng = rand::rng();
let lines = large_sample_lines();
let state = build_dissolve_state(&lines, &mut rng);
let start = state.tile_starts[0];
let end_tick = start + state.tile_garble_durations[0] + 1;
let after = dissolve_char_from_state('▓', 0, end_tick, &state, &mut rng);
assert_eq!(after, ' ');
}
#[test]
fn reveal_char_at_space_returns_space() {
let lines = sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let ch = reveal_char_at(' ', 0, 0, 100, 0, &map, &mut rng);
assert_eq!(ch, ' ');
}
#[test]
fn reveal_char_at_resolved_returns_original() {
let lines = large_sample_lines();
let map = build_distance_map(&lines);
let mut rng = rand::rng();
let ch = reveal_char_at('▓', 10, 19, 50000, 0, &map, &mut rng);
assert_eq!(ch, '▓');
}
#[test]
fn core_char_at_within_radius() {
let mut rng = rand::rng();
let core_row = (ANCHOR_ROW + CORE_ROW_OFFSET) as usize;
let anchor_col = ANCHOR_COL as usize;
let mut found_garble = false;
for _ in 0..50 {
let ch = core_char_at(core_row, anchor_col, core_grow_ticks(), &mut rng);
if ch != ' ' {
found_garble = true;
break;
}
}
assert!(found_garble);
}
#[test]
fn core_char_at_outside_radius() {
let mut rng = rand::rng();
let ch = core_char_at(0, 0, 0, &mut rng);
assert_eq!(ch, ' ');
}
}