commit afaf8d669df4ff2b19b81c14e2d279b031b60286 Author: Stanislav Nikolov Date: Fri Aug 5 20:36:35 2022 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9026c77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.vscode diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b74b647 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,192 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bulls-and-cows" +version = "0.1.0" +dependencies = [ + "clap", + "rand", + "text_io", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" + +[[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.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "text_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb170b4f47dc48835fbc56259c12d8963e542b05a24be2e3a1f5a6c320fd2d4" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b3d064b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bulls-and-cows" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.4" +text_io = "0.1.8" +clap = "2.33.3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9093283 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 Stanislav Nikolov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..70dfc1a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,309 @@ +use std::{ + collections::HashSet, + convert::TryInto, + fs::File, + io::{self, BufRead, Write}, +}; + +use clap::{App, Arg, ArgMatches}; +use rand::Rng; +use text_io::read; + +type Guess = String; + +struct GuessComparator {} + +impl GuessComparator { + fn compare<'a>(target: &Guess, guess: Guess) -> GuessResult { + let mut bulls = 0; + let mut target_cows = HashSet::new(); + let mut guess_cows = HashSet::new(); + + for ab in target.chars().into_iter().zip(guess.chars()) { + if ab.0 == ab.1 { + bulls += 1; + } else { + target_cows.insert(ab.0); + guess_cows.insert(ab.1); + } + } + let cows = target_cows.intersection(&guess_cows).count().try_into().unwrap(); + + GuessResult::new(guess, bulls, cows) + } + + fn is_consistent_with_guesses( + guess: &Guess, + guess_vec: &[GuessResult], + ) -> bool { + !guess_vec + .iter() + .any(|g| &Self::compare(guess, g.guess.clone()) != g) + } +} + +trait GuessGenerator { + fn with_len(len: u32) -> Self + where + Self: Sized; + fn get_len(&self) -> u32; + fn generate(&self) -> Guess; + + fn parse(&self, s: &str) -> Result; + + fn is_win(&self, guess: &GuessResult) -> bool { + self.get_len() == guess.bulls + } +} + +struct WordGuessGenerator { + len: u32, + words: Vec, +} + +impl GuessGenerator for WordGuessGenerator { + fn with_len(len: u32) -> Self + where + Self: Sized, + { + const DICT_FILENAME: &str = "/usr/share/dict/words"; + + let file = File::open(DICT_FILENAME).unwrap(); + let words: Vec<_> = io::BufReader::new(file) + .lines() + .filter(|line| { + if let Ok(line) = line { + let mut digits = HashSet::new(); + line.chars().for_each(|c| { + digits.insert(c); + }); + + line.len() == len.try_into().unwrap() + && line.len() == digits.len() + && digits.into_iter().all(|d| ('a'..='z').contains(&d)) + } else { + false + } + }) + .map(|line| line.unwrap().to_lowercase()) + .collect(); + + Self { len, words } + } + + fn get_len(&self) -> u32 { + self.len + } + + fn generate(&self) -> Guess { + let index = rand::thread_rng().gen_range(0..self.words.len()); + self.words[index].clone() + } + + fn parse(&self, s: &str) -> Result { + if self.words.contains(&s.to_string()) { + Ok(Guess::from(s)) + } else { + Err("Invalid value") + } + } +} + +struct NumberGuessGenerator { + len: u32, +} + +impl GuessGenerator for NumberGuessGenerator { + fn with_len(len: u32) -> Self { + Self { len } + } + + fn get_len(&self) -> u32 { + self.len + } + + fn parse(&self, s: &str) -> Result { + let mut digits = HashSet::new(); + s.chars().for_each(|c| { + digits.insert(c); + }); + + if s.len() == self.len.try_into().unwrap() + && s.len() == digits.len() + && digits.into_iter().all(|d| ('1'..='9').contains(&d)) + { + Ok(Guess::from(s)) + } else { + Err("Not a valid value") + } + } + + fn generate(&self) -> Guess { + let mut rng = rand::thread_rng(); + loop { + let low: i32 = 10i32.pow(self.len - 1); + let high: i32 = 10i32.pow(self.len); + let n = rng.gen_range(low..high); + + let guess = self.parse(&n.to_string()); + if let Ok(guess) = guess { + return guess; + } + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +struct GuessResult { + guess: Guess, + bulls: u32, + cows: u32, +} + +impl GuessResult { + fn new(guess: Guess, bulls: u32, cows: u32) -> GuessResult { + GuessResult { + guess, + bulls, + cows, + } + } +} + +fn run_ai_guess_program(gen: Box, number: &str) { + let number = gen.parse(number).expect("Invalid number"); + let mut guess_vec = Vec::new(); + loop { + let guess = gen.generate(); + if !GuessComparator::is_consistent_with_guesses(&guess, &guess_vec) { + continue; + } + + let result = GuessComparator::compare(&number, guess); + + if gen.is_win(&result) { + println!("{}.", result.guess); + break; + } else { + println!("{} -> {} bulls, {} cows", result.guess, result.bulls, result.cows); + }; + + guess_vec.push(result); + } +} + +fn run_user_guess_program(gen: Box) { + let number = gen.generate(); + let mut guess_vec = Vec::new(); + + loop { + let guess: String; + loop { + print!("Enter a guess: "); + std::io::stdout().flush().unwrap(); + + let g: String = read!("{}"); + if g == "?" { + println!("The answer was: {}", number); + return; + } else if g == "q" { + return; + } else if let Ok(g) = gen.parse(&g) { + guess = g; + break; + } else { + println!(); + } + } + + let result = GuessComparator::compare(&number, guess); + let is_win = gen.is_win(&result); + guess_vec.push(result); + guess_vec.iter().enumerate().for_each(|(i, g)| { + println!( + "{}. {} -> {} bulls, {} cows", + i + 1, + g.guess, + g.bulls, + g.cows + ) + }); + + if is_win { + println!("You win!"); + break; + } + } +} + +fn read_args() -> ArgMatches<'static> { + App::new("Bulls and Cows") + .version("1.0") + .author("Stanislav Nikolov ") + .about("A bulls and cows implementation.") + .arg( + Arg::with_name("variant") + .required(true) + .possible_values(&["number", "words"]) + .short("variant") + .long("variant") + .value_name("VARIANT") + .help("Sets the game variant") + .takes_value(true), + ) + .arg( + Arg::with_name("length") + .long("length") + .value_name("LENGTH") + .help("Sets the guess length") + .default_value("4") + ) + .arg( + Arg::with_name("run-ai") + .long("run-ai") + .value_name("GUESS") + .help("Runs the AI against the given guess") + .takes_value(true) + ) + .get_matches() +} +fn main() { + let args = read_args(); + + let run_ai = args.value_of("run-ai"); + let len: u32 = if let Some(guess) = run_ai { + guess.len().try_into().unwrap() + } else { + args.value_of("length").unwrap().parse().unwrap() + }; + let gen: Box = match args.value_of("variant").unwrap() { + "number" => Box::new(NumberGuessGenerator::with_len(len)), + "words" => Box::new(WordGuessGenerator::with_len(len)), + _ => panic!("Unknown variant: {}", args.value_of("variant").unwrap()), + }; + + if let Some(guess) = run_ai { + run_ai_guess_program(gen, guess); + } else { + run_user_guess_program(gen); + } +} + +#[cfg(test)] +mod test { + use crate::{Guess, GuessResult, GuessComparator}; + + #[test] + fn test_matches() { + let g1234 = Guess::from("1234"); + let g1432 = Guess::from("1432"); + let g4321 = Guess::from("4321"); + let g5678 = Guess::from("5678"); + + assert_eq!(GuessComparator::compare(&g1234, g5678.clone()), GuessResult::new(g5678.clone(), 0, 0)); + assert_eq!(GuessComparator::compare(&g1234, g5678.clone()), GuessResult::new(g5678.clone(), 0, 0)); + assert_eq!(GuessComparator::compare(&g1234, g4321.clone()), GuessResult::new(g4321.clone(), 0, 4)); + assert_eq!(GuessComparator::compare(&g1234, g1432.clone()), GuessResult::new(g1432.clone(), 2, 2)); + assert_eq!(GuessComparator::compare(&g1234, g1234.clone()), GuessResult::new(g1234.clone(), 4, 0)); + } +}