Skip to content

Commit

Permalink
Generate words based on an initial difficulty (#3)
Browse files Browse the repository at this point in the history
* generate some words

* added support for specifying a gamemode and seeing some initial word generation

* rearrange the comments and remove what work I've already done

* add a first demo gif
  • Loading branch information
scottnm committed Jan 23, 2021
1 parent 07366a5 commit bf43b7c
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 24 deletions.
73 changes: 72 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Binary file added demo/01-generate-words.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 9 additions & 2 deletions src/dict.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
word_set: Vec<String>,
}

impl EnglishDictChunk {
Expand All @@ -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<usize>) -> String {
let word_index = rng.gen_range(0, self.word_set.len());
self.word_set[word_index].clone()
}
}
144 changes: 126 additions & 18 deletions src/game.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Self::Err> {
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<usize>) -> Vec<String> {
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
Expand All @@ -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);
}
}
}
}
15 changes: 12 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String>),
}

Expand All @@ -32,7 +34,14 @@ fn parse_cmdline_args() -> Result<CmdlineArgs, &'static str> {
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::<game::Difficulty>()?;
Mode::Game(parsed_difficulty)
}
_ => return Err("Invalid mode argument"),
};

Expand All @@ -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)
}
Expand Down
Loading

0 comments on commit bf43b7c

Please sign in to comment.