e
This commit is contained in:
parent
0023a73b83
commit
7e2fa309cd
7 changed files with 613 additions and 218 deletions
45
.vscode/launch.json
vendored
Normal file
45
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug executable 'optimle'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=optimle",
|
||||||
|
"--package=optimle"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "optimle",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in executable 'optimle'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--bin=optimle",
|
||||||
|
"--package=optimle"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "optimle",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
211
src/constraints.rs
Normal file
211
src/constraints.rs
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use crate::guess::*;
|
||||||
|
|
||||||
|
pub trait Constraints<const N: usize> {
|
||||||
|
fn push(&mut self, result: GuessResult<N>);
|
||||||
|
fn check(&self, word: Word<N>) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LetterCount<const N: usize> {
|
||||||
|
letter_minimums: HashMap<char, usize>,
|
||||||
|
letter_maximums: HashMap<char, usize>,
|
||||||
|
empty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> LetterCount<N> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
LetterCount {
|
||||||
|
letter_minimums: HashMap::new(),
|
||||||
|
letter_maximums: HashMap::new(),
|
||||||
|
empty: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Constraints<N> for LetterCount<N> {
|
||||||
|
fn push(&mut self, result: GuessResult<N>) {
|
||||||
|
self.empty = false;
|
||||||
|
|
||||||
|
let mut letter_matches = HashMap::new();
|
||||||
|
for a in 0..N {
|
||||||
|
letter_matches
|
||||||
|
.entry(result.word[a])
|
||||||
|
.or_insert(Vec::new())
|
||||||
|
.push(result.matches[a]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (letter, matches) in letter_matches {
|
||||||
|
let nowheres = matches.iter().filter(|&m| *m == LetterMatch::NOWHERE).count();
|
||||||
|
let somewheres = matches.len() - nowheres;
|
||||||
|
self.letter_minimums.insert(
|
||||||
|
letter,
|
||||||
|
std::cmp::max(
|
||||||
|
*self.letter_minimums.get(&letter).unwrap_or(&0),
|
||||||
|
somewheres,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if nowheres > 0 {
|
||||||
|
self.letter_maximums.insert(
|
||||||
|
letter,
|
||||||
|
std::cmp::min(
|
||||||
|
*self.letter_maximums.get(&letter).unwrap_or(&N),
|
||||||
|
somewheres,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self, word: Word<N>) -> bool {
|
||||||
|
if self.empty { return true; }
|
||||||
|
|
||||||
|
let mut letter_counts = HashMap::new();
|
||||||
|
for a in 0..N {
|
||||||
|
*letter_counts.entry(word[a]).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (letter, minimum) in &self.letter_minimums {
|
||||||
|
let letter_count = letter_counts.get(&letter).unwrap_or(&0);
|
||||||
|
if letter_count < minimum {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (letter, maximum) in &self.letter_maximums {
|
||||||
|
let letter_count = letter_counts.get(&letter).unwrap_or(&0);
|
||||||
|
if letter_count > maximum {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LetterPosition<const N: usize> {
|
||||||
|
heres: [Option<char>; N],
|
||||||
|
elsewheres: [Vec<char>; N],
|
||||||
|
empty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> LetterPosition<N> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
heres: [None; N],
|
||||||
|
elsewheres: vec![Vec::new(); N].try_into().expect("failed to initialize [Vec<char>; N]"),
|
||||||
|
empty: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Constraints<N> for LetterPosition<N> {
|
||||||
|
fn push(&mut self, result: GuessResult<N>) {
|
||||||
|
self.empty = false;
|
||||||
|
|
||||||
|
for a in 0..N {
|
||||||
|
match result.matches[a] {
|
||||||
|
LetterMatch::HERE => self.heres[a] = Some(result.word[a]),
|
||||||
|
LetterMatch::ELSEWHERE => {
|
||||||
|
self.elsewheres[a].push(result.word[a]);
|
||||||
|
for b in 0..N {
|
||||||
|
if a != b && result.word[b] == result.word[a] && result.matches[b] == LetterMatch::NOWHERE {
|
||||||
|
self.elsewheres[b].push(result.word[a]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LetterMatch::NOWHERE => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self, word: Word<N>) -> bool {
|
||||||
|
if self.empty { return true; }
|
||||||
|
|
||||||
|
for a in 0..N {
|
||||||
|
if let Some(here) = self.heres[a] {
|
||||||
|
if word[a] != here {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.elsewheres[a].contains(&word[a]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Total<const N: usize> {
|
||||||
|
count: LetterCount<N>,
|
||||||
|
position: LetterPosition<N>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Total<N> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
count: LetterCount::new(),
|
||||||
|
position: LetterPosition::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Constraints<N> for Total<N> {
|
||||||
|
fn push(&mut self, result: GuessResult<N>) {
|
||||||
|
self.count.push(result);
|
||||||
|
self.position.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self, word: Word<N>) -> bool {
|
||||||
|
self.count.check(word) && self.position.check(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_constrains_nowheres() {
|
||||||
|
let mut constraints = Total::new();
|
||||||
|
constraints.push(GuessResult {
|
||||||
|
word: word!("loose"),
|
||||||
|
matches: [LetterMatch::NOWHERE; 5],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(false, constraints.check(word!("blink"))); // no L
|
||||||
|
assert_eq!(false, constraints.check(word!("ghost"))); // no O
|
||||||
|
assert_eq!(true, constraints.check(word!("chain")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_constraints_elsewheres() {
|
||||||
|
let mut constraints = Total::new();
|
||||||
|
constraints.push(GuessResult {
|
||||||
|
word: word!("loose"),
|
||||||
|
matches: [LetterMatch::NOWHERE, LetterMatch::ELSEWHERE, LetterMatch::NOWHERE, LetterMatch::NOWHERE, LetterMatch::NOWHERE],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(false, constraints.check(word!("brood"))); // too many O's
|
||||||
|
assert_eq!(false, constraints.check(word!("going"))); // O can't go here
|
||||||
|
assert_eq!(false, constraints.check(word!("abort"))); // O can't go here
|
||||||
|
assert_eq!(true, constraints.check(word!("favor")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_constrains_heres() {
|
||||||
|
let mut constraints = Total::new();
|
||||||
|
constraints.push(GuessResult {
|
||||||
|
word: word!("loose"),
|
||||||
|
matches: [LetterMatch::NOWHERE, LetterMatch::HERE, LetterMatch::HERE, LetterMatch::HERE, LetterMatch::HERE],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(false, constraints.check(word!("house"))); // no U here
|
||||||
|
assert_eq!(false, constraints.check(word!("loose"))); // no L here
|
||||||
|
assert_eq!(true, constraints.check(word!("goose")));
|
||||||
|
}
|
||||||
|
}
|
163
src/evaluator.rs
Normal file
163
src/evaluator.rs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::guess::*;
|
||||||
|
use crate::constraints;
|
||||||
|
use constraints::Constraints;
|
||||||
|
|
||||||
|
pub trait Evaluator<const N: usize> {
|
||||||
|
fn push(&mut self, _result: GuessResult<N>) {}
|
||||||
|
fn evaluate(&self, word: Word<N>) -> Matches<N>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SecretEvaluator<const N: usize> {
|
||||||
|
secret: Word<N>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> SecretEvaluator<N> {
|
||||||
|
pub fn new(secret: Word<N>) -> Self {
|
||||||
|
Self {
|
||||||
|
secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Evaluator<N> for SecretEvaluator<N> {
|
||||||
|
fn evaluate(&self, word: Word<N>) -> Matches<N> {
|
||||||
|
guess(&word, &self.secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AdversarialEvaluator<const N: usize> {
|
||||||
|
possible_words: Dictionary<N>,
|
||||||
|
constraints: constraints::Total<N>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> AdversarialEvaluator<N> {
|
||||||
|
pub fn new(dictionary: &Dictionary<N>) -> Self {
|
||||||
|
Self {
|
||||||
|
possible_words: dictionary.to_vec(),
|
||||||
|
constraints: constraints::Total::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Evaluator<N> for AdversarialEvaluator<N> {
|
||||||
|
fn evaluate(&self, word: Word<N>) -> Matches<N> {
|
||||||
|
let mut evaluated_matches: HashSet<Matches<N>> = HashSet::new();
|
||||||
|
let mut best_secret: Option<Word<N>> = None;
|
||||||
|
let mut best_possible_words_count: Option<usize> = None;
|
||||||
|
let mut resulting_constraints = constraints::Total::new();
|
||||||
|
|
||||||
|
for possible_secret in &self.possible_words {
|
||||||
|
let matches = guess(&word, &possible_secret);
|
||||||
|
if !evaluated_matches.insert(matches) { continue; }
|
||||||
|
|
||||||
|
resulting_constraints.clone_from(&self.constraints);
|
||||||
|
resulting_constraints.push(GuessResult { word, matches });
|
||||||
|
|
||||||
|
let possible_words_count = if *possible_secret == word {
|
||||||
|
0 // Special case to avoid giving the win when 2 possible words are left
|
||||||
|
} else {
|
||||||
|
self.possible_words
|
||||||
|
.iter()
|
||||||
|
.filter(|&word| resulting_constraints.check(*word))
|
||||||
|
.count()
|
||||||
|
};
|
||||||
|
|
||||||
|
if best_possible_words_count.is_none() || possible_words_count > best_possible_words_count.unwrap() {
|
||||||
|
best_possible_words_count = Some(possible_words_count);
|
||||||
|
best_secret = Some(*possible_secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return guess(&word, &best_secret.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, result: GuessResult<N>) {
|
||||||
|
self.constraints.push(result);
|
||||||
|
let constraints = &self.constraints;
|
||||||
|
self.possible_words.retain(|&word| constraints.check(word));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_evaluates_adversarially() {
|
||||||
|
let dictionary: Vec<Word<2>> = vec![
|
||||||
|
word!("am"), word!("an"), word!("as"), word!("at"), word!("ax"),
|
||||||
|
word!("in"), word!("is"), word!("it"),
|
||||||
|
word!("on"), word!("ox"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let adversary = AdversarialEvaluator::new(&dictionary);
|
||||||
|
|
||||||
|
// Locking in the 'a' leaves 4 possibilities;
|
||||||
|
// excluding it would leave 2 with the 'n' or 3 without
|
||||||
|
assert_eq!(
|
||||||
|
[LetterMatch::HERE, LetterMatch::NOWHERE],
|
||||||
|
adversary.evaluate(word!("an")),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The 'm' doesn't eliminate any non-'a' words,
|
||||||
|
// so excluding the 'a' leaves all 5 of them
|
||||||
|
assert_eq!(
|
||||||
|
[LetterMatch::NOWHERE, LetterMatch::NOWHERE],
|
||||||
|
adversary.evaluate(word!("am")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_adheres_to_constraints() {
|
||||||
|
let dictionary: Vec<Word<2>> = vec![
|
||||||
|
word!("am"), word!("an"), word!("as"), word!("at"), word!("ax"),
|
||||||
|
word!("in"), word!("is"), word!("it"),
|
||||||
|
word!("on"), word!("ox"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut adversary = AdversarialEvaluator::new(&dictionary);
|
||||||
|
adversary.push(GuessResult {
|
||||||
|
word: word!("an"),
|
||||||
|
matches: [LetterMatch::NOWHERE, LetterMatch::NOWHERE],
|
||||||
|
});
|
||||||
|
|
||||||
|
// We have already excluded both of these letters
|
||||||
|
assert_eq!(
|
||||||
|
[LetterMatch::NOWHERE, LetterMatch::NOWHERE],
|
||||||
|
adversary.evaluate(word!("an")),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Locking in the 'i' leaves 2 possibilities;
|
||||||
|
// excluding it would only leave 'ox'
|
||||||
|
assert_eq!(
|
||||||
|
[LetterMatch::HERE, LetterMatch::NOWHERE],
|
||||||
|
adversary.evaluate(word!("in")),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Play it out
|
||||||
|
adversary.push(GuessResult {
|
||||||
|
word: word!("in"),
|
||||||
|
matches: [LetterMatch::HERE, LetterMatch::NOWHERE],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Leaves 'it'
|
||||||
|
assert_eq!(
|
||||||
|
[LetterMatch::HERE, LetterMatch::NOWHERE],
|
||||||
|
adversary.evaluate(word!("is")),
|
||||||
|
);
|
||||||
|
|
||||||
|
adversary.push(GuessResult {
|
||||||
|
word: word!("is"),
|
||||||
|
matches: [LetterMatch::HERE, LetterMatch::NOWHERE],
|
||||||
|
});
|
||||||
|
|
||||||
|
// No choice but to accept
|
||||||
|
assert_eq!(
|
||||||
|
[LetterMatch::HERE, LetterMatch::HERE],
|
||||||
|
adversary.evaluate(word!("it")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
19
src/game.rs
Normal file
19
src/game.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::guess::*;
|
||||||
|
use crate::guesser;
|
||||||
|
use crate::evaluator;
|
||||||
|
|
||||||
|
pub struct Game<'a, const N: usize> {
|
||||||
|
pub guesser: &'a mut dyn guesser::Guesser<N>,
|
||||||
|
pub evaluator: &'a mut dyn evaluator::Evaluator<N>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const N: usize> Game<'a, N> {
|
||||||
|
pub fn next(&mut self) -> GuessResult<N> {
|
||||||
|
let word = self.guesser.guess();
|
||||||
|
let matches = self.evaluator.evaluate(word);
|
||||||
|
let result = GuessResult { word, matches };
|
||||||
|
self.guesser.push(result);
|
||||||
|
self.evaluator.push(result);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
109
src/guess.rs
Normal file
109
src/guess.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum LetterMatch {
|
||||||
|
NOWHERE,
|
||||||
|
ELSEWHERE,
|
||||||
|
HERE,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Word<const N: usize> = [char; N];
|
||||||
|
pub type Matches<const N: usize> = [LetterMatch; N];
|
||||||
|
|
||||||
|
pub fn guess<const N: usize>(word: &Word<N>, secret: &Word<N>) -> Matches<N> {
|
||||||
|
let mut result = [LetterMatch::NOWHERE; N];
|
||||||
|
let mut matched_secret_letters = [false; N];
|
||||||
|
|
||||||
|
// Find exact matches
|
||||||
|
for a in 0..N {
|
||||||
|
if word[a] == secret[a] {
|
||||||
|
result[a] = LetterMatch::HERE;
|
||||||
|
matched_secret_letters[a] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find wrong-position matches
|
||||||
|
for a in 0..N {
|
||||||
|
for b in 0..N {
|
||||||
|
if a == b || result[a] != LetterMatch::NOWHERE || matched_secret_letters[b] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if word[a] == secret[b] {
|
||||||
|
result[a] = LetterMatch::ELSEWHERE;
|
||||||
|
matched_secret_letters[b] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct GuessResult<const N: usize> {
|
||||||
|
pub word: Word<N>,
|
||||||
|
pub matches: Matches<N>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> GuessResult<N> {
|
||||||
|
pub fn correct(&self) -> bool {
|
||||||
|
for a in 0..N {
|
||||||
|
if self.matches[a] != LetterMatch::HERE {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> std::fmt::Display for GuessResult<N> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for a in 0..N {
|
||||||
|
result.push(match self.matches[a] {
|
||||||
|
LetterMatch::ELSEWHERE => format!("({})", self.word[a]),
|
||||||
|
LetterMatch::HERE => format!("[{}]", self.word[a]),
|
||||||
|
LetterMatch::NOWHERE => format!(" {} ", self.word[a]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
write!(f, "{}", result.join(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type GuessHistory<const N: usize> = Vec<GuessResult<N>>;
|
||||||
|
pub type Dictionary<const N: usize> = Vec<Word<N>>;
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_matches_correct_guess() {
|
||||||
|
let secret: Word<5> = word!("loose");
|
||||||
|
assert_eq!([LetterMatch::HERE; 5], guess(&secret, &secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_matches_all_elsewhere() {
|
||||||
|
let secret = word!("loose");
|
||||||
|
let word = word!("seloo"); // lol
|
||||||
|
assert_eq!([LetterMatch::ELSEWHERE; 5], guess(&word, &secret));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_matches_all_nowhere() {
|
||||||
|
let secret = word!("loose");
|
||||||
|
let word = word!("brink");
|
||||||
|
assert_eq!([LetterMatch::NOWHERE; 5], guess(&word, &secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_matches_close_guess() {
|
||||||
|
let secret = word!("loose");
|
||||||
|
let word = word!("loves");
|
||||||
|
let expected = [LetterMatch::HERE, LetterMatch::HERE, LetterMatch::NOWHERE, LetterMatch::ELSEWHERE, LetterMatch::ELSEWHERE];
|
||||||
|
assert_eq!(expected, guess(&word, &secret));
|
||||||
|
}
|
||||||
|
}
|
41
src/guesser.rs
Normal file
41
src/guesser.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
|
use crate::guess::*;
|
||||||
|
use crate::constraints;
|
||||||
|
|
||||||
|
pub trait Guesser<const N: usize> {
|
||||||
|
fn push(&mut self, _result: GuessResult<N>) {}
|
||||||
|
fn guess(&self) -> Word<N>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RandomConstraintGuesser<'a, const N: usize> {
|
||||||
|
possible_words: Dictionary<N>,
|
||||||
|
history: GuessHistory<N>,
|
||||||
|
constraints: &'a mut dyn constraints::Constraints<N>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const N: usize> RandomConstraintGuesser<'a, N> {
|
||||||
|
pub fn new(
|
||||||
|
dictionary: &Dictionary<N>,
|
||||||
|
constraints: &'a mut dyn constraints::Constraints<N>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
possible_words: dictionary.to_vec(),
|
||||||
|
history: GuessHistory::new(),
|
||||||
|
constraints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const N: usize> Guesser<N> for RandomConstraintGuesser<'a, N> {
|
||||||
|
fn guess(&self) -> Word<N> {
|
||||||
|
*self.possible_words.choose(&mut rand::thread_rng()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, result: GuessResult<N>) {
|
||||||
|
self.history.push(result);
|
||||||
|
self.constraints.push(result);
|
||||||
|
let constraints = &self.constraints;
|
||||||
|
self.possible_words.retain(|&word| constraints.check(word));
|
||||||
|
}
|
||||||
|
}
|
241
src/main.rs
241
src/main.rs
|
@ -1,230 +1,35 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
#[macro_export]
|
||||||
enum LetterMatch {
|
macro_rules! word {
|
||||||
NOWHERE,
|
( $x:expr ) => {
|
||||||
ELSEWHERE,
|
{
|
||||||
HERE,
|
$x.chars()
|
||||||
}
|
.collect::<Vec<char>>()
|
||||||
|
.as_slice()
|
||||||
type Word<const N: usize> = [char; N];
|
.try_into()
|
||||||
type Matches<const N: usize> = [LetterMatch; N];
|
.unwrap()
|
||||||
|
|
||||||
fn guess<const N: usize>(word: &Word<N>, secret: &Word<N>) -> Matches<N> {
|
|
||||||
let mut result = [LetterMatch::NOWHERE; N];
|
|
||||||
let mut matched_secret_letters = [false; N];
|
|
||||||
|
|
||||||
// Find exact matches
|
|
||||||
for a in 0..N {
|
|
||||||
if word[a] == secret[a] {
|
|
||||||
result[a] = LetterMatch::HERE;
|
|
||||||
matched_secret_letters[a] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find wrong-position matches
|
|
||||||
for a in 0..N {
|
|
||||||
for b in 0..N {
|
|
||||||
if a == b || matched_secret_letters[b] {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if word[a] == secret[b] {
|
|
||||||
result[a] = LetterMatch::ELSEWHERE;
|
|
||||||
matched_secret_letters[b] = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
mod guess;
|
||||||
}
|
use guess::*;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
mod constraints;
|
||||||
struct GuessResult<const N: usize> {
|
mod game;
|
||||||
word: Word<N>,
|
mod guesser;
|
||||||
matches: Matches<N>,
|
mod evaluator;
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> GuessResult<N> {
|
|
||||||
fn correct(&self) -> bool {
|
|
||||||
for a in 0..N {
|
|
||||||
if self.matches[a] != LetterMatch::HERE {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> std::fmt::Display for GuessResult<N> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for a in 0..N {
|
|
||||||
result.push(match self.matches[a] {
|
|
||||||
LetterMatch::ELSEWHERE => format!("({})", self.word[a]),
|
|
||||||
LetterMatch::HERE => format!("[{}]", self.word[a]),
|
|
||||||
LetterMatch::NOWHERE => format!(" {} ", self.word[a]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
write!(f, "{}", result.join(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type GuessHistory<const N: usize> = Vec<GuessResult<N>>;
|
|
||||||
type Dictionary<const N: usize> = Vec<Word<N>>;
|
|
||||||
|
|
||||||
trait Evaluator<const N: usize> {
|
|
||||||
fn push(&mut self, _result: GuessResult<N>) {}
|
|
||||||
fn evaluate(&self, word: Word<N>) -> Matches<N>;
|
|
||||||
}
|
|
||||||
|
|
||||||
trait Guesser<const N: usize> {
|
|
||||||
fn push(&mut self, _result: GuessResult<N>) {}
|
|
||||||
fn guess(&self) -> Word<N>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct SecretEvaluator<const N: usize> {
|
fn play<const N: usize>(dictionary: &Dictionary<N>) {
|
||||||
secret: Word<N>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> SecretEvaluator<N> {
|
|
||||||
pub fn new(secret: Word<N>) -> Self {
|
|
||||||
Self {
|
|
||||||
secret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> Evaluator<N> for SecretEvaluator<N> {
|
|
||||||
fn evaluate(&self, word: Word<N>) -> [LetterMatch; N] {
|
|
||||||
guess(&word, &self.secret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct LetterConstraints<const N: usize> {
|
|
||||||
letter_minimums: HashMap<char, usize>,
|
|
||||||
letter_maximums: HashMap<char, usize>,
|
|
||||||
empty: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> LetterConstraints<N> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
letter_minimums: HashMap::new(),
|
|
||||||
letter_maximums: HashMap::new(),
|
|
||||||
empty: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check(&self, word: Word<N>) -> bool {
|
|
||||||
if self.empty { return true; }
|
|
||||||
|
|
||||||
let mut letter_counts = HashMap::new();
|
|
||||||
for a in 0..N {
|
|
||||||
*letter_counts.entry(word[a]).or_insert(0) += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (letter, count) in letter_counts {
|
|
||||||
if count < *self.letter_minimums.get(&letter).unwrap_or(&0)
|
|
||||||
|| count > *self.letter_maximums.get(&letter).unwrap_or(&N) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> LetterConstraints<N> {
|
|
||||||
fn push(&mut self, result: GuessResult<N>) {
|
|
||||||
self.empty = false;
|
|
||||||
|
|
||||||
let mut letter_matches = HashMap::new();
|
|
||||||
for a in 0..N {
|
|
||||||
letter_matches
|
|
||||||
.entry(result.word[a])
|
|
||||||
.or_insert(Vec::new())
|
|
||||||
.push(result.matches[a]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (letter, matches) in letter_matches {
|
|
||||||
let nowheres = matches.iter().filter(|&m| *m == LetterMatch::NOWHERE).count();
|
|
||||||
let somewheres = matches.len() - nowheres;
|
|
||||||
self.letter_minimums.insert(
|
|
||||||
letter,
|
|
||||||
std::cmp::max(
|
|
||||||
*self.letter_minimums.get(&letter).unwrap_or(&0),
|
|
||||||
somewheres,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if nowheres > 0 {
|
|
||||||
self.letter_maximums.insert(
|
|
||||||
letter,
|
|
||||||
std::cmp::min(
|
|
||||||
*self.letter_maximums.get(&letter).unwrap_or(&N),
|
|
||||||
somewheres,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct RandomEasyGuesser<const N: usize> {
|
|
||||||
dictionary: Dictionary<N>,
|
|
||||||
history: GuessHistory<N>,
|
|
||||||
constraints: LetterConstraints<N>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> RandomEasyGuesser<N> {
|
|
||||||
pub fn new(dictionary: Dictionary<N>) -> Self {
|
|
||||||
Self {
|
|
||||||
dictionary,
|
|
||||||
history: GuessHistory::new(),
|
|
||||||
constraints: LetterConstraints::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> Guesser<N> for RandomEasyGuesser<N> {
|
|
||||||
fn guess(&self) -> Word<N> {
|
|
||||||
*self.dictionary.choose(&mut rand::thread_rng()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push(&mut self, result: GuessResult<N>) {
|
|
||||||
self.history.push(result);
|
|
||||||
self.constraints.push(result);
|
|
||||||
let constraints = &self.constraints;
|
|
||||||
self.dictionary.retain(|&word| constraints.check(word));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct Game<'a, const N: usize> {
|
|
||||||
guesser: &'a mut dyn Guesser<N>,
|
|
||||||
evaluator: &'a mut dyn Evaluator<N>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, const N: usize> Game<'a, N> {
|
|
||||||
fn next(&mut self) -> GuessResult<N> {
|
|
||||||
let word = self.guesser.guess();
|
|
||||||
let matches = self.evaluator.evaluate(word);
|
|
||||||
let result = GuessResult { word, matches };
|
|
||||||
self.guesser.push(result);
|
|
||||||
self.evaluator.push(result);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play<const N: usize>(dictionary: Dictionary<N>) {
|
|
||||||
let secret = *dictionary.choose(&mut rand::thread_rng()).unwrap();
|
let secret = *dictionary.choose(&mut rand::thread_rng()).unwrap();
|
||||||
let mut game = Game {
|
println!("The secret is {:?}", secret);
|
||||||
evaluator: &mut SecretEvaluator::new(secret),
|
let mut constraints = constraints::Total::new();
|
||||||
guesser: &mut RandomEasyGuesser::new(dictionary),
|
let mut game = game::Game {
|
||||||
|
evaluator: &mut evaluator::AdversarialEvaluator::new(dictionary),
|
||||||
|
guesser: &mut guesser::RandomConstraintGuesser::new(dictionary, &mut constraints),
|
||||||
};
|
};
|
||||||
loop {
|
loop {
|
||||||
let result = game.next();
|
let result = game.next();
|
||||||
|
@ -250,5 +55,7 @@ fn load_words<const N: usize>(contents: &'static str) -> Vec<Word<N>> {
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let common_words: Vec<Word<5>> = load_words(include_str!("words/common.txt"));
|
let common_words: Vec<Word<5>> = load_words(include_str!("words/common.txt"));
|
||||||
play(common_words);
|
loop {
|
||||||
|
play(&common_words);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue