Initial commit

This commit is contained in:
Stanislav Nikolov 2022-08-05 20:36:35 +03:00
commit afaf8d669d
5 changed files with 533 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.vscode

192
Cargo.lock generated Normal file
View File

@ -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"

11
Cargo.toml Normal file
View File

@ -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"

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2022 Stanislav Nikolov <hello@snikolov.me>
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.

309
src/main.rs Normal file
View File

@ -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<Guess, &'static str>;
fn is_win(&self, guess: &GuessResult) -> bool {
self.get_len() == guess.bulls
}
}
struct WordGuessGenerator {
len: u32,
words: Vec<String>,
}
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<Guess, &'static str> {
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<Guess, &'static str> {
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<dyn GuessGenerator>, 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<dyn GuessGenerator>) {
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 <hello@snikolov.me>")
.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<dyn GuessGenerator> = 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));
}
}