This commit is contained in:
Matthieu Jolimaitre 2024-10-25 19:20:11 +02:00
commit b130df0391
16 changed files with 339925 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

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 = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "libc"
version = "0.2.160"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[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.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "syn"
version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "turbotusmors"
version = "0.1.0"
dependencies = [
"rand",
"rayon",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "turbotusmors"
version = "0.1.0"
edition = "2021"
default-run = "proxy"
[dependencies]
rand = "0.8.5"
rayon = "1.10.0"
[[bin]]
name = "proxy"
path = "src/proxy.rs"
[[bin]]
name = "simulate"
path = "src/simulate.rs"
[[bin]]
name = "bench"
path = "src/bench.rs"

3
build.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn main() {
//
}

20276
data/francais.txt Normal file

File diff suppressed because it is too large Load diff

318885
data/gutenberg.txt Normal file

File diff suppressed because it is too large Load diff

2
rustfmt.toml Normal file
View file

@ -0,0 +1,2 @@
hard_tabs = true
max_width = 120

35
src/bench.rs Normal file
View file

@ -0,0 +1,35 @@
#![allow(dead_code, unused)]
// use core::iter::repeat;
use game::{simulation::Simulation, Game};
use rand::seq::SliceRandom;
use rayon::iter::repeat;
use rayon::prelude::*;
use solve::grouping::Grouping;
use std::env::args;
mod dictionnary;
mod game;
mod solve;
fn main() {
let count = args()
.nth(1)
.map(|s| s.parse().expect("Number expected."))
.unwrap_or(100);
// let dict = dictionnary::francais(7);
let dict = dictionnary::gutenberg(7);
(0..count).into_par_iter().for_each(|_| {
let target = dict.choose(&mut rand::thread_rng()).unwrap().to_string();
dbg!(&target);
let solver = Grouping::new(dict.clone());
let mut game = Simulation::new(target.clone());
let result = game.play_all(solver, Some(20));
match result {
Ok((_, tries)) => println!("succ,{target},{tries}"),
Err(reason) => println!("fail,{target},{reason}"),
};
});
}
// fail,suedois,Reached limit.

30
src/dictionnary.rs Normal file
View file

@ -0,0 +1,30 @@
use std::{fs, path::Path};
use rand::{seq::SliceRandom, thread_rng};
pub fn load(path: impl AsRef<Path>, width: usize) -> Vec<String> {
let content = fs::read_to_string(path).expect("Can read file.");
content
.lines()
.filter(|w| w.len() == width)
.map(|l| l.to_string())
.collect()
}
pub fn francais(width: usize) -> Vec<String> {
let raw = include_str!("../data/francais.txt");
raw.lines()
.filter(|w| w.len() == width)
.map(|l| l.to_string())
.collect()
}
pub fn gutenberg(width: usize) -> Vec<String> {
let raw = include_str!("../data/gutenberg.txt");
let mut res = raw
.lines()
.filter(|w| w.len() == width)
.map(|l| l.to_string())
.collect::<Vec<_>>();
res.shuffle(&mut thread_rng());
res
}

103
src/game/mod.rs Normal file
View file

@ -0,0 +1,103 @@
use std::ops::Not;
use crate::solve::Solver;
pub mod proxy;
pub mod simulation;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InfoKind {
There,
Elsewhere,
Abscent,
}
impl InfoKind {
pub fn to_printable(self) -> char {
match self {
InfoKind::Abscent => '.',
InfoKind::Elsewhere => '-',
InfoKind::There => '+',
}
}
pub fn from_printable(printable: char) -> Option<Self> {
Some(match printable {
'.' => InfoKind::Abscent,
'-' => InfoKind::Elsewhere,
'+' => InfoKind::There,
_ => None?,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct Info {
pub kind: InfoKind,
pub character: char,
}
impl Info {
pub fn new(kind: InfoKind, character: char) -> Self {
Self { kind, character }
}
pub fn empty() -> Self {
Self::new(InfoKind::Abscent, '#')
}
pub fn as_printable(infos: impl Iterator<Item = Self>) -> Vec<char> {
infos.map(|i| i.kind.to_printable()).collect()
}
pub fn from_printable(
printables: impl Iterator<Item = char> + Clone,
characters: impl Iterator<Item = char>,
) -> Option<Vec<Self>> {
let result = printables.map(InfoKind::from_printable);
let success = result.clone().any(|o| o.is_none()).not();
success.then(|| result.zip(characters).map(|(r, c)| Self::new(r.unwrap(), c)).collect())
}
pub fn from_init(characters: impl Iterator<Item = char>) -> Vec<Self> {
characters
.map(|c| match c {
'.' => Self::new(InfoKind::Abscent, '#'),
_ => Self::new(InfoKind::There, c),
})
.collect()
}
pub fn to_word(infos: impl Iterator<Item = Self>) -> String {
infos.map(|i| i.character).collect()
}
}
pub trait Game {
fn length(&mut self) -> usize;
fn guess(&mut self, word: &str) -> Result<(), Vec<Info>>;
fn play_all(&mut self, mut solver: impl Solver, mut limit: Option<usize>) -> Result<(String, usize), String> {
for try_ in 0.. {
if let Some(limit) = limit {
if limit == try_ {
return Err("Reached limit.".into());
}
}
let guess = solver.guess();
match guess {
None => return Err("".into()),
Some(guess) => match self.guess(dbg!(&guess)) {
Ok(()) => return Ok((guess, try_)),
Err(infos) => {
let printable = Info::as_printable(infos.iter().cloned())
.into_iter()
.collect::<String>();
dbg!(printable);
solver.learn(infos);
}
},
}
}
unreachable!()
}
}

43
src/game/proxy.rs Normal file
View file

@ -0,0 +1,43 @@
use std::io::{self, Write};
use super::{Game, Info};
pub struct Proxy {
length: usize,
}
impl Proxy {
pub fn init() -> (Self, Vec<Info>) {
println!("Infos.");
let infos = prompt();
let infos = Info::from_init(infos.chars());
let length = infos.len();
(Self { length }, infos)
}
}
impl Game for Proxy {
fn guess(&mut self, word: &str) -> Result<(), Vec<Info>> {
println!("Guessing");
println!(" {word}");
loop {
let result = prompt();
let Some(infos) = Info::from_printable(result.chars(), word.chars()) else {
continue;
};
return Err(infos);
}
}
fn length(&mut self) -> usize {
self.length
}
}
fn prompt() -> String {
print!("> ");
io::stdout().flush().ok();
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
line.trim_end_matches("\n").to_string()
}

80
src/game/simulation.rs Normal file
View file

@ -0,0 +1,80 @@
use rayon::prelude::*;
use crate::game::Info;
use super::{Game, InfoKind};
pub struct Simulation {
target: String,
}
impl Simulation {
pub fn new(target: impl ToString) -> Self {
let target = target.to_string();
Self { target }
}
}
impl Game for Simulation {
fn guess(&mut self, word: &str) -> Result<(), Vec<Info>> {
if word == self.target {
Ok(())
} else {
Err(wordle(&self.target, word))
}
}
fn length(&mut self) -> usize {
self.target.len()
}
}
pub fn wordle(target: &str, word: &str) -> Vec<Info> {
let mut buffs = wordle_buffs_for(target);
wordle_inner(target, word, &mut buffs);
buffs.2
}
pub fn wordle_buffs_for(target: &str) -> (Vec<Option<InfoKind>>, Vec<Option<char>>, Vec<Info>) {
let infos = target.chars().map(|_| None).collect();
let letters = target.chars().map(Some).collect();
let result = target.chars().map(|_| Info::empty()).collect();
(infos, letters, result)
}
pub fn wordle_inner(target: &str, word: &str, buffs: &mut (Vec<Option<InfoKind>>, Vec<Option<char>>, Vec<Info>)) {
fn to_info(letter: &mut Option<char>, info: &mut Option<InfoKind>, kind: InfoKind) {
*letter = None;
info.insert(kind);
}
let (infos, letters, result) = buffs;
let d = drop;
infos.iter_mut().for_each(|i| d(i.take()));
let d = drop;
letters.iter_mut().zip(target.chars()).for_each(|(l, c)| d(l.insert(c)));
word.chars()
.zip(letters.iter_mut())
.zip(infos.iter_mut())
.filter_map(|((w, l), i)| l.is_some_and(|t| w == t).then_some((l, i)))
.for_each(|(r, i)| to_info(r, i, InfoKind::There));
word.chars()
.zip(infos.iter_mut())
.filter(|(r, i)| i.is_none())
.for_each(|(l, i)| {
let r = letters.iter_mut().find(|e| e.as_ref().is_some_and(|e| *e == l));
if let Some(r) = r {
to_info(r, i, InfoKind::Elsewhere);
}
});
infos
.iter()
.map(|i| i.unwrap_or(InfoKind::Abscent))
.zip(word.chars())
.map(|(i, c)| Info::new(i, c))
.zip(result.iter_mut())
.for_each(|(i, r)| *r = i);
}

20
src/proxy.rs Normal file
View file

@ -0,0 +1,20 @@
#![allow(dead_code, unused)]
use game::{proxy::Proxy, Game};
use solve::{
grouping::{matches, Grouping},
Solver,
};
mod dictionnary;
mod game;
mod solve;
pub fn main() {
let (mut game, infos) = Proxy::init();
let dict = dictionnary::gutenberg(infos.len());
let mut solver = Grouping::new(dict.into_iter().filter(|w| matches(w, &infos)));
solver.learn(infos);
let result = game.play_all(solver, Some(5));
println!("{result:?}");
}

23
src/simulate.rs Normal file
View file

@ -0,0 +1,23 @@
#![allow(dead_code, unused)]
use std::env::args;
use game::{simulation::Simulation, Game};
use rand::seq::SliceRandom;
use solve::grouping::Grouping;
mod dictionnary;
mod game;
mod solve;
fn main() {
let first_arg = args().nth(1);
let dict = dictionnary::gutenberg(first_arg.clone().map(|w| w.len()).unwrap_or(7));
let random_word = || dict.choose(&mut rand::thread_rng()).unwrap().to_string();
let target = first_arg.unwrap_or_else(random_word);
dbg!(&target);
let solver = Grouping::new(dict);
let mut game = Simulation::new(target);
let result = game.play_all(solver, Some(20));
println!("{result:?}");
}

203
src/solve/grouping.rs Normal file
View file

@ -0,0 +1,203 @@
use std::{cmp::Ordering, collections::HashSet};
use rand::{
seq::{IteratorRandom, SliceRandom},
thread_rng,
};
use rayon::iter::{ParallelBridge, ParallelIterator};
use crate::{
dictionnary::{self, gutenberg},
game::{
simulation::{wordle, wordle_buffs_for, wordle_inner},
Info, InfoKind,
},
};
use super::Solver;
#[derive(Debug)]
pub struct Grouping {
dict: Vec<String>,
candidates: Vec<String>,
}
impl Grouping {
pub fn new<S: ToString>(dict: impl IntoIterator<Item = S>) -> Self {
let dict = dict.into_iter().map(|s| s.to_string()).collect::<Vec<_>>();
let candidates = dict.clone();
Self { dict, candidates }
}
/// Gives a score to the word (lowest is best).
fn rank_word(&self, word: &str) -> usize {
let mut groups = [0; 3];
let mut buffs = wordle_buffs_for(word);
for target in &self.candidates {
wordle_inner(target, word, &mut buffs);
let infos = &buffs.2;
for i in infos {
match i.kind {
InfoKind::Abscent => groups[0] += 1,
InfoKind::Elsewhere => groups[1] += 1,
InfoKind::There => groups[2] += 1,
}
}
}
let min = groups.iter().min().unwrap();
let max = groups.iter().max().unwrap();
(max - min)
}
}
impl Solver for Grouping {
fn guess(&mut self) -> Option<String> {
if self.candidates.len() == 1 {
return self.candidates.first().cloned();
}
self.dict
.iter()
.take(10_000)
.par_bridge()
.map(|word| (word, self.rank_word(word)))
.min_by_key(|(_, score)| *score)
.map(|(w, _)| w.to_string())
.or_else(|| self.candidates.first().cloned())
}
fn learn(&mut self, infos: Vec<Info>) {
let word = Info::to_word(infos.clone().into_iter());
if let Some(index) = self
.dict
.iter()
.enumerate()
.find_map(|(i, w)| (*w == *word).then_some(i))
{
self.dict.remove(index);
}
self.candidates = self.candidates.drain(..).filter(|w| matches(w, &infos)).collect();
dbg!(&self.candidates);
dbg!(self.candidates.len());
}
}
pub fn matches(word: &str, infos: &[Info]) -> bool {
let results = word
.chars()
.zip(infos)
.map(|(w, info)| match (w, info.kind, info.character) {
(w, InfoKind::There, c) if w == c => (None, None),
(_, InfoKind::There, _) => (Some(false), None),
(w, InfoKind::Elsewhere, c) if w == c => (Some(false), None),
(w, _, _) => (None, Some((info, w))),
});
if let Some(conclusion) = results.clone().filter_map(|(c, _)| c).next() {
return conclusion;
}
let remaining = results.filter_map(|(_, maybe)| maybe);
let mut remaining_letters = remaining.clone().map(|(_, w)| Some(w)).collect::<Vec<_>>();
let remaining_info = remaining.map(|(i, _)| i);
for info in remaining_info {
let found = remaining_letters
.iter_mut()
.find(|l| l.is_some_and(|l| l == info.character));
match (info.kind, found) {
(InfoKind::Abscent, Some(_)) => return false,
(InfoKind::Abscent, _) => continue,
(InfoKind::Elsewhere, None) => return false,
(InfoKind::Elsewhere, Some(found)) => drop(found.take()),
(InfoKind::There, _) => unreachable!(),
}
}
true
}
#[test]
fn test_misc() {
// let grouping = Grouping::new(["cocon", "coton"]);
// assert!(grouping.rank_word("coton") > grouping.rank_word("abaca"));
let grouping = Grouping::new(["abregee", "amorcee", "marquee", "assuree", "separee"]);
dbg!(grouping.rank_word("volitif"));
dbg!(grouping.rank_word("amorcee"));
let mut gut = gutenberg(7);
dbg!(gut.len());
let mut grouping = Grouping::new(gut.into_iter().filter(|w| w.starts_with('j')));
grouping.learn(vec![
Info {
kind: InfoKind::There,
character: 'j',
},
Info {
kind: InfoKind::Elsewhere,
character: 'u',
},
Info {
kind: InfoKind::Abscent,
character: 'v',
},
Info {
kind: InfoKind::Elsewhere,
character: 'e',
},
Info {
kind: InfoKind::Abscent,
character: 'n',
},
Info {
kind: InfoKind::Abscent,
character: 'a',
},
Info {
kind: InfoKind::Elsewhere,
character: 't',
},
]);
dbg!(grouping.rank_word("jouxtee"));
dbg!(grouping.rank_word("jutions"));
dbg!(grouping.rank_word("jaspine"));
dbg!(grouping.guess());
}
#[test]
fn test_matches() {
let cases = [
("gargouille", "gargouille", "+....-...-", false),
("gargouille", "guetteuses", "+....-...-", true),
("grognement", "grognement", "+....+.-.-", false),
("grognement", "guetteuses", "+....+.-.-", true),
("gesticuler", "gesticuler", "+--+..+.+.", false),
("gesticuler", "guetteuses", "+--+..+.+.", true),
("guetteuses", "guetteuses", "++++++++++", true),
("inoculeras", "inoculeras", "+-.-+.+-..", false),
("inoculeras", "imprudence", "+-.-+.+-..", true),
("implosions", "implosions", "+++.....-.", false),
("implosions", "imprudence", "+++.....-.", true),
("imprudents", "imprudents", "++++++++..", false),
("imprudents", "imprudence", "++++++++..", true),
("imprudence", "imprudence", "++++++++++", true),
("cocon", "coton", "++.++", true),
("coton", "coton", "+++++", true),
];
for (tried, word, infos, matches_) in cases {
let infos_ = Info::from_printable(infos.chars(), tried.chars()).expect("Info deserialized correctly.");
assert_eq!(
matches(word, &infos_),
matches_,
"Asserts that matching '{}' against {} ({}) returns {}.\n{}\n{}\n{}",
word,
infos,
tried,
matches_,
word,
infos,
tried,
);
}
}

8
src/solve/mod.rs Normal file
View file

@ -0,0 +1,8 @@
use crate::game::Info;
pub trait Solver {
fn learn(&mut self, infos: Vec<Info>);
fn guess(&mut self) -> Option<String>;
}
pub mod grouping;