use core::f64; use eyre::Result; use rand::{rngs::ThreadRng, Rng}; use std::{ collections::HashMap, io::{self, Write}, sync::mpsc::{Receiver, Sender}, }; #[derive(Copy, Clone, Eq, PartialEq, Debug)] enum Hand { Left, Right, } #[derive(Copy, Clone, Eq, PartialEq, Debug)] enum Digit { Pinky, Ring, Middle, Index, } #[derive(Copy, Clone, Eq, PartialEq, Debug)] struct Finger { hand: Hand, digit: Digit, } const MATRIX_COUNT: usize = 30; macro_rules! F { ($h:ident, $d:ident) => { Finger { hand: Hand::$h, digit: Digit::$d, } }; } const LPINKY: Finger = F!(Left, Pinky); const LRING: Finger = F!(Left, Ring); const LMIDDLE: Finger = F!(Left, Middle); const LINDEX: Finger = F!(Left, Index); const RPINKY: Finger = F!(Right, Pinky); const RRING: Finger = F!(Right, Ring); const RMIDDLE: Finger = F!(Right, Middle); const RINDEX: Finger = F!(Right, Index); #[rustfmt::skip] const MATRIX_EFFORT: &[f64; MATRIX_COUNT] = &[ 100., 6., 10., 6., 40., 40., 10., 10., 10., 100., 50., 2., 0.1, 0.1, 15., 15., 0.1, 0.1, 2., 50., 10., 14., 10., 20., 40., 30., 6., 14., 6., 100., ]; #[rustfmt::skip] const MATRIX_FINGERS: &[Finger; MATRIX_COUNT] = &[ LPINKY, LRING, LMIDDLE, LINDEX, LINDEX, RINDEX, RINDEX, RMIDDLE, RRING, RPINKY, LPINKY, LRING, LMIDDLE, LINDEX, LINDEX, RINDEX, RINDEX, RMIDDLE, RRING, RPINKY, LRING, LMIDDLE, LINDEX, LINDEX, LINDEX, RINDEX, RINDEX, RMIDDLE, RRING, RPINKY, ]; const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyz.,/'"; const _: () = assert!(CHARACTERS.len() == MATRIX_COUNT); const fn char_data_max_index() -> usize { let bytes = CHARACTERS.as_bytes(); let mut max = 0; let len = bytes.len(); let mut i = 0; while i < len { let b = bytes[i]; max = if b < max { max } else { b }; i += 1; } max as usize + 1 } // https://www3.nd.edu/~busiforc/handouts/cryptography/Letter%20Frequencies.html#Relative_frequencies_of_letters const CHAR_FREQ_LOOKUP: [f64; char_data_max_index()] = { let mut data: [f64; char_data_max_index()] = [0.; char_data_max_index()]; macro_rules! d { ($ch:expr, $val:expr) => {{ data[$ch as usize] = $val * 1000.; }}; } d!('a', 0.08167); d!('b', 0.01492); d!('c', 0.02782); d!('d', 0.04253); d!('e', 0.12702); d!('f', 0.02228); d!('g', 0.02015); d!('h', 0.06094); d!('i', 0.06966); d!('j', 0.00153); d!('k', 0.00772); d!('l', 0.04025); d!('m', 0.02406); d!('n', 0.06749); d!('o', 0.07507); d!('p', 0.01929); d!('q', 0.00095); d!('r', 0.05987); d!('s', 0.06327); d!('t', 0.09056); d!('u', 0.02758); d!('v', 0.00978); d!('w', 0.02360); d!('x', 0.00150); d!('y', 0.01974); d!('z', 0.00074); data }; #[derive(Copy, Clone)] struct Layout { key_indices: [usize; char_data_max_index()], } impl Layout { const fn from_key_matrix(m: &[char; MATRIX_COUNT]) -> Self { let mut key_indices = [0; char_data_max_index()]; let mut i = 0; while i < m.len() { let ch = m[i]; key_indices[ch as usize] = i; i += 1; } Self { key_indices } } fn to_key_matrix(self) -> [char; MATRIX_COUNT] { let mut out = ['0'; MATRIX_COUNT]; for ch in CHARACTERS.chars() { out[self.key_indices[ch as usize]] = ch; } out } fn key_to_xy(&self, ch: char) -> (u8, u8) { let index = self.key_indices[ch as usize]; let y = index / 10; let x = index % 10; (x as u8, y as u8) } fn balance_penalty(&self) -> f64 { let km = self.to_key_matrix(); let mut left_freq = 0.; for x in 0..5 { for y in 0..3 { let i = y * 10 + x; let ch = km[i]; left_freq += CHAR_FREQ_LOOKUP[ch as usize]; } } let mut right_freq = 0.; for x in 5..10 { for y in 0..3 { let i = y * 10 + x; let ch = km[i]; right_freq += CHAR_FREQ_LOOKUP[ch as usize]; } } (left_freq - right_freq).abs() * 10. } } #[test] fn test_key_to_xy() { assert_eq!(INITIAL_LAYOUT.key_to_xy('g'), (4, 1)); } fn key_effort(ch: char, l: &Layout) -> f64 { CHAR_FREQ_LOOKUP[ch as usize] * MATRIX_EFFORT[l.key_indices[ch as usize]] } /* #[rustfmt::skip] const INITIAL_LAYOUT: Layout = Layout::from_key_matrix(&[ 'q', 'w', 'f', 'p', 'b', 'j', 'l', 'u', 'y', '\'', 'a', 'r', 's', 't', 'g', 'm', 'n', 'e', 'i', 'o', 'x', 'c', 'd', 'v', 'z', 'k', 'h', ',', '.', '/', ]); */ #[rustfmt::skip] const INITIAL_LAYOUT: Layout = Layout::from_key_matrix(&[ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\'', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', ]); type NGramFreqs = HashMap<&'static str, f64>; static mut BIGRAM_FREQS: Option = None; unsafe fn init_bigram_freqs() { let freqs = [ ("th", 0.03882543), ("he", 0.03681), ("in", 0.02283899), ("er", 0.02178042), ("an", 0.02140460), ("re", 0.01749394), ("nd", 0.01571977), ("on", 0.01418244), ("en", 0.01383239), ("at", 0.01335523), ("ou", 0.01285485), ("ed", 0.01275779), ("ha", 0.01274742), ("to", 0.01169655), ("or", 0.01151094), ("it", 0.01134891), ("is", 0.01109877), ("hi", 0.01092302), ("es", 0.01092301), ("ng", 0.01053385), ] .into_iter() .map(|(s, v)| (s, v * 4000.)) .collect::(); unsafe { BIGRAM_FREQS = Some(freqs) }; } #[allow(static_mut_refs)] fn bigram_effort(bigram: &str, l: &Layout) -> f64 { let bigrams = unsafe { BIGRAM_FREQS.as_ref().unwrap_unchecked() }; let mut eff = bigrams.get(bigram).copied().unwrap_or(1.); let ch1 = bigram.as_bytes()[0]; let ch2 = bigram.as_bytes()[1]; let finger1 = MATRIX_FINGERS[l.key_indices[ch1 as usize]]; let finger2 = MATRIX_FINGERS[l.key_indices[ch2 as usize]]; let ch1xy = l.key_to_xy(ch1 as char); let ch2xy = l.key_to_xy(ch2 as char); let x_diff = ch1xy.0.abs_diff(ch2xy.0); let y_diff = ch1xy.1.abs_diff(ch2xy.1); if finger1 == finger2 { if y_diff > 1 { eff *= 1000.; } else { eff *= 500.; } } if finger1.hand != finger2.hand { eff /= 2.; } else if x_diff == 1 { let mult = if (finger1.hand == Hand::Left && ch1xy.0 < ch2xy.0) || (finger1.hand == Hand::Right && ch1xy.0 > ch2xy.0) { 10. } else { 1. }; if y_diff == 0 { eff /= 1000. * mult; } else { eff /= 250. * mult; } } else { eff *= (x_diff + 1) as f64; eff *= (y_diff + 1) as f64; } eff } static mut ALL_BIGRAMS: Option> = None; unsafe fn init_all_bigrams() { let it = CHARACTERS.chars().flat_map(|ch1| { CHARACTERS .chars() .map(move |ch2| [ch1 as u8, ch2 as u8]) .map(|b| String::from_utf8(b.to_vec()).unwrap()) }); unsafe { ALL_BIGRAMS = Some(it.collect()) } } #[allow(static_mut_refs)] fn all_bigrams() -> &'static [String] { unsafe { ALL_BIGRAMS.as_ref().unwrap_unchecked() } } const N_WORKERS: usize = 6; fn mutate_layout(layout: &mut Layout, rng: &mut ThreadRng, max_swaps: usize) { let num_swaps = rng.gen_range(1..=max_swaps); for _ in 0..num_swaps { let ch1 = CHARACTERS .chars() .nth(rng.gen_range(0..CHARACTERS.len())) .unwrap(); let ch2 = CHARACTERS .chars() .nth(rng.gen_range(0..CHARACTERS.len())) .unwrap(); layout.key_indices.swap(ch1 as usize, ch2 as usize); } } struct WorkerThreadInit { initial_layout: Layout, report: Sender<(f64, Layout)>, /// The maximum amount of deviation from the min_effort value allowed before /// reverting back to the currently-known best layout max_dev: f64, /// The maximum number of swaps to make between iterations max_swaps: usize, } fn worker_thread_run(init: WorkerThreadInit) { let mut rng = rand::thread_rng(); let mut layout = init.initial_layout; let mut best_layout = layout; let mut min_effort: f64 = f64::MAX; loop { let mut eff: f64 = CHARACTERS.chars().map(|v| key_effort(v, &layout)).sum(); eff += all_bigrams() .iter() .map(|b| bigram_effort(b, &layout)) .sum::(); eff *= layout.balance_penalty(); if eff < min_effort { min_effort = eff; best_layout = layout; init.report.send((eff, layout)).unwrap(); } else if (eff - min_effort) > init.max_dev { layout = best_layout; } mutate_layout(&mut layout, &mut rng, init.max_swaps); } } fn print_layout(layout: Layout) { let km = layout.to_key_matrix(); for chunk in km.chunks(MATRIX_COUNT / 3) { for ch in chunk { print!("{ch}"); } println!(); } } fn clear_screen() { print!("\x1B[2J\x1B[H"); io::stdout().flush().unwrap(); } fn ui_thread_run(rx: Receiver<(f64, Layout)>) { for (eff, layout) in rx.iter() { clear_screen(); print_layout(layout); println!(); println!("{eff}"); } } fn aggregator_thread_run( layout: Layout, rx: Receiver<(f64, Layout)>, tx: Sender<(f64, Layout)>, ) { let mut best_layout = layout; let mut min_effort = f64::MAX; for (eff, layout) in rx.iter() { if eff < min_effort { best_layout = layout; min_effort = eff; tx.send((min_effort, best_layout)).unwrap(); } } } fn main() -> Result<()> { unsafe { init_all_bigrams(); init_bigram_freqs(); }; let (tx_agg, rx_agg) = std::sync::mpsc::channel(); let (tx_ui, rx_ui) = std::sync::mpsc::channel(); let ui_thread = std::thread::spawn(|| { ui_thread_run(rx_ui); }); let aggregator_thread = std::thread::spawn(|| { aggregator_thread_run(INITIAL_LAYOUT, rx_agg, tx_ui); }); let worker_threads = (0..N_WORKERS) .map(|_| { let tx = tx_agg.clone(); std::thread::spawn(move || { worker_thread_run(WorkerThreadInit { initial_layout: INITIAL_LAYOUT, report: tx, max_dev: 1000., max_swaps: 10, }) }) }) .collect::>(); ui_thread.join().unwrap(); aggregator_thread.join().unwrap(); for j in worker_threads { j.join().unwrap(); } Ok(()) }