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 rand::seq::SliceRandom;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
enum LetterMatch {
|
||||
NOWHERE,
|
||||
ELSEWHERE,
|
||||
HERE,
|
||||
}
|
||||
|
||||
type Word<const N: usize> = [char; N];
|
||||
type Matches<const N: usize> = [LetterMatch; N];
|
||||
|
||||
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;
|
||||
#[macro_export]
|
||||
macro_rules! word {
|
||||
( $x:expr ) => {
|
||||
{
|
||||
$x.chars()
|
||||
.collect::<Vec<char>>()
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
mod guess;
|
||||
use guess::*;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct GuessResult<const N: usize> {
|
||||
word: Word<N>,
|
||||
matches: Matches<N>,
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
mod constraints;
|
||||
mod game;
|
||||
mod guesser;
|
||||
mod evaluator;
|
||||
|
||||
|
||||
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>) -> [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>) {
|
||||
fn play<const N: usize>(dictionary: &Dictionary<N>) {
|
||||
let secret = *dictionary.choose(&mut rand::thread_rng()).unwrap();
|
||||
let mut game = Game {
|
||||
evaluator: &mut SecretEvaluator::new(secret),
|
||||
guesser: &mut RandomEasyGuesser::new(dictionary),
|
||||
println!("The secret is {:?}", secret);
|
||||
let mut constraints = constraints::Total::new();
|
||||
let mut game = game::Game {
|
||||
evaluator: &mut evaluator::AdversarialEvaluator::new(dictionary),
|
||||
guesser: &mut guesser::RandomConstraintGuesser::new(dictionary, &mut constraints),
|
||||
};
|
||||
loop {
|
||||
let result = game.next();
|
||||
|
@ -250,5 +55,7 @@ fn load_words<const N: usize>(contents: &'static str) -> Vec<Word<N>> {
|
|||
|
||||
fn main() {
|
||||
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