diff --git a/Cargo.lock b/Cargo.lock index a3f8376..9441dbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,15 +12,33 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "fonv_cracker" version = "0.1.0" dependencies = [ "pancurses", + "rand", "snm_simple_file", "text_io", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + [[package]] name = "libc" version = "0.2.82" @@ -33,7 +51,7 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", ] [[package]] @@ -76,6 +94,53 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + [[package]] name = "snm_simple_file" version = "0.1.0" @@ -87,6 +152,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cb170b4f47dc48835fbc56259c12d8963e542b05a24be2e3a1f5a6c320fd2d4" +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 3122085..08e04c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ edition = "2018" snm_simple_file = { git = "https://github.com/scottnm/snm_simple_file", branch = "main" } text_io = "0.1.8" pancurses = "0.16.1" +rand = "0.7.0" diff --git a/README.md b/README.md index 8e89c03..0da8a14 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,9 @@ Currently this tool is a helper utility for cracking fallout: new vegas terminal The plan is to extend it to also be able to play a clone of the cracking game. ![Tests](https://github.com/scottnm/fonv-cracker/workflows/Tests/badge.svg) + +## Game Demo Progress + +- [generate words](https://github.com/scottnm/fonv-cracker/FILLME) + +![Animation of words being generated](demo/01-generate-words.gif) diff --git a/demo/01-generate-words.gif b/demo/01-generate-words.gif new file mode 100644 index 0000000..52bf695 Binary files /dev/null and b/demo/01-generate-words.gif differ diff --git a/src/dict.rs b/src/dict.rs index 02104ee..37a477f 100644 --- a/src/dict.rs +++ b/src/dict.rs @@ -1,8 +1,10 @@ +use crate::randwrapper::RangeRng; + // Each dict chunk represents all words of the same length from our src dict. This partitioning is a // quick optimization since the cracker game will only concern itself with words of the same length. pub struct EnglishDictChunk { word_len: usize, - word_set: std::collections::HashSet, + word_set: Vec, } impl EnglishDictChunk { @@ -14,6 +16,11 @@ impl EnglishDictChunk { pub fn is_word(&self, word: &str) -> bool { assert_eq!(self.word_len, word.len()); - self.word_set.contains(word) + self.word_set.iter().any(|word_in_set| word_in_set == word) + } + + pub fn get_random_word(&self, rng: &mut dyn RangeRng) -> String { + let word_index = rng.gen_range(0, self.word_set.len()); + self.word_set[word_index].clone() } } diff --git a/src/game.rs b/src/game.rs index 386defb..ca64786 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,6 +1,81 @@ +// Work breakdown +// - constrain the number of words generated to only as much as would fit in two panes +// - select a word to be the solution word +// - run a window-less gameloop which lets us input words and get back the results of "N matching chars to solution" +// - setup the win-lose condition that you only have 4 guesses +// - render two panes +// - place the words throughout the pane w/ filler text that goes in between words +// - add support for selecting between the words in the TUI and highlighting the current selection +// - mouse support? +// - keyboard support? +// - add support for using that selection instead of text input to power the gameloop +// - add an output pane which tells you the results of your current selection + +// extensions/flavor +// - use appropriate font to give it a "fallout feel" +// - SFX + +use crate::dict; +use crate::randwrapper::{RangeRng, ThreadRangeRng}; +use std::str::FromStr; + const TITLE: &str = "FONV: Terminal Cracker"; -pub fn run_game() { +#[derive(Debug, Clone, Copy)] +pub enum Difficulty { + VeryEasy, + Easy, + Average, + Hard, + VeryHard, +} + +impl FromStr for Difficulty { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("VeryEasy") || s.eq_ignore_ascii_case("VE") { + return Ok(Difficulty::VeryEasy); + } + + if s.eq_ignore_ascii_case("Easy") || s.eq_ignore_ascii_case("E") { + return Ok(Difficulty::Easy); + } + + if s.eq_ignore_ascii_case("Average") || s.eq_ignore_ascii_case("A") { + return Ok(Difficulty::Average); + } + + if s.eq_ignore_ascii_case("Hard") || s.eq_ignore_ascii_case("H") { + return Ok(Difficulty::Hard); + } + + if s.eq_ignore_ascii_case("VeryHard") || s.eq_ignore_ascii_case("VH") { + return Ok(Difficulty::VeryHard); + } + + Err("Invalid difficulty string") + } +} + +pub fn generate_words(difficulty: Difficulty, rng: &mut dyn RangeRng) -> Vec { + let word_len = match difficulty { + Difficulty::VeryEasy => 4, + Difficulty::Easy => 6, + Difficulty::Average => 8, + Difficulty::Hard => 10, + Difficulty::VeryHard => 12, + }; + + const WORDS_TO_GENERATE_COUNT: usize = 16; + + let dict_chunk = dict::EnglishDictChunk::load(word_len); + (0..WORDS_TO_GENERATE_COUNT) + .map(|_| dict_chunk.get_random_word(rng)) + .collect() +} + +pub fn run_game(difficulty: Difficulty) { // setup the window let window = pancurses::initscr(); pancurses::noecho(); // prevent key inputs rendering to the screen @@ -10,26 +85,59 @@ pub fn run_game() { window.nodelay(true); // don't block waiting for key inputs (we'll poll) window.keypad(true); // let special keys be captured by the program (i.e. esc/backspace/del/arrow keys) + let mut rng = ThreadRangeRng::new(); + let rand_words = generate_words(difficulty, &mut rng); + // TODO just open a stub window for now. We'll write the game soon. window.clear(); + + window.mvaddstr(0, 0, format!("{:?}", difficulty)); + for (i, rand_word) in rand_words.iter().enumerate() { + window.mvaddstr(i as i32 + 1, 0, rand_word); + } + window.refresh(); - std::thread::sleep(std::time::Duration::from_millis(1000)); + std::thread::sleep(std::time::Duration::from_millis(3000)); } -// Work breakdown -// - generate a set of words given a difficulty/length -// - constrain the number of words generated to only as much as would fit in two panes -// - select a word to be the solution word -// - run a window-less gameloop which lets us input words and get back the results of "N matching chars to solution" -// - setup the win-lose condition that you only have 4 guesses -// - render two panes -// - place the words throughout the pane w/ filler text that goes in between words -// - add support for selecting between the words in the TUI and highlighting the current selection -// - mouse support? -// - keyboard support? -// - add support for using that selection instead of text input to power the gameloop -// - add an output pane which tells you the results of your current selection +#[cfg(test)] +mod tests { + use super::*; + use crate::randwrapper; -// extensions/flavor -// - use appropriate font to give it a "fallout feel" -// - SFX + #[test] + fn test_word_generation() { + let mut rng = randwrapper::mocks::SequenceRangeRng::new(&[0, 2, 4, 7]); + let tests = [ + (Difficulty::VeryEasy, ["aahs", "aani", "abac", "abba"]), + (Difficulty::Easy, ["aahing", "aarrgh", "abacay", "abacot"]), + ( + Difficulty::Average, + ["aardvark", "aaronite", "abacisci", "abacuses"], + ), + ( + Difficulty::Hard, + ["aardwolves", "abalienate", "abandoning", "abaptistum"], + ), + ( + Difficulty::VeryHard, + [ + "abalienating", + "abandonments", + "abbreviately", + "abbreviatory", + ], + ), + ]; + + for (difficulty, expected_words) in &tests { + let generated_words = generate_words(*difficulty, &mut rng); + let expected_word_cnt = 16; + for i in 0..expected_word_cnt { + let generated_word = &generated_words[i]; + let expected_word = expected_words[i % expected_words.len()]; + assert_eq!(generated_word, expected_word); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 274069c..f143a8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,15 @@ extern crate pancurses; +extern crate rand; extern crate snm_simple_file; mod dict; mod game; +mod randwrapper; mod solver; #[derive(Debug)] enum Mode { - Game, + Game(game::Difficulty), Solver(String, Vec), } @@ -32,7 +34,14 @@ fn parse_cmdline_args() -> Result { let known_guess_args = args.iter().skip(2).map(|a| a.clone()).collect(); Mode::Solver(args[1].clone(), known_guess_args) } - "--game" => Mode::Game, + "--game" => { + if args.len() < 2 { + return Err("Missing difficulty arg for game mode"); + } + + let parsed_difficulty = args[1].parse::()?; + Mode::Game(parsed_difficulty) + } _ => return Err("Invalid mode argument"), }; @@ -54,7 +63,7 @@ fn main() { }; match args.mode { - Mode::Game => game::run_game(), + Mode::Game(difficulty) => game::run_game(difficulty), Mode::Solver(input_password_file, known_guess_args) => { solver::solver(&input_password_file, &known_guess_args) } diff --git a/src/randwrapper.rs b/src/randwrapper.rs new file mode 100644 index 0000000..5cac1b1 --- /dev/null +++ b/src/randwrapper.rs @@ -0,0 +1,99 @@ +use rand::Rng; + +pub trait RangeRng { + fn gen_range(&mut self, lower: T, upper: T) -> T; +} + +pub struct ThreadRangeRng { + rng: rand::rngs::ThreadRng, +} + +impl ThreadRangeRng { + pub fn new() -> ThreadRangeRng { + ThreadRangeRng { + rng: rand::thread_rng(), + } + } +} + +impl RangeRng for ThreadRangeRng { + fn gen_range(&mut self, lower: T, upper: T) -> T { + self.rng.gen_range(lower, upper) + } +} + +#[cfg(test)] +pub mod mocks { + use super::*; + + pub struct SingleValueRangeRng { + value: T, + } + + pub struct SequenceRangeRng { + next: usize, + seq: Vec, + } + + impl SingleValueRangeRng { + pub fn new(value: T) -> SingleValueRangeRng { + SingleValueRangeRng { value } + } + } + + impl RangeRng for SingleValueRangeRng { + fn gen_range(&mut self, lower: T, upper: T) -> T { + assert!(lower <= self.value); + assert!(upper > self.value); + self.value + } + } + + impl SequenceRangeRng { + pub fn new(value: &[T]) -> SequenceRangeRng { + SequenceRangeRng { + next: 0, + seq: Vec::from(value), + } + } + } + + impl RangeRng for SequenceRangeRng { + fn gen_range(&mut self, lower: T, upper: T) -> T { + let value = self.seq[self.next]; + self.next = (self.next + 1) % self.seq.len(); + + assert!(lower <= value); + assert!(upper > value); + value + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn gen_wrapper(rng: &mut dyn RangeRng, lower: T, upper: T) -> T { + rng.gen_range(lower, upper) + } + + #[test] + fn test_thread_random() { + // this test is mostly here to verify that things compile + let mut rng = ThreadRangeRng::new(); + let first_value = rng.gen_range(0, 10); + let next_value = gen_wrapper(&mut rng, 10, 20); + assert_ne!(first_value, next_value); + } + + #[test] + fn test_single_value_random() { + let mut rng = mocks::SingleValueRangeRng::new(10i32); + let first_value = rng.gen_range(0, 100); + for _ in 1..10 { + let next_value = gen_wrapper(&mut rng, 0, 100); + assert_eq!(first_value, next_value); + } + } +}