candidate sets for the fill
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
use crate::dict::Dictionary;
|
||||
|
||||
pub fn score_candidate(
|
||||
word_idx: usize,
|
||||
dict: &Dictionary,
|
||||
used_words: &[bool],
|
||||
placed_texts: &[String],
|
||||
) -> f64 {
|
||||
if used_words[word_idx] {
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
let word = dict.get(word_idx);
|
||||
let text = &word.text;
|
||||
let mut score = word.frequency;
|
||||
|
||||
// reject words that are too similar to already-placed words
|
||||
for placed in placed_texts {
|
||||
if is_related(text, placed) {
|
||||
return -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// bonus for letter variety
|
||||
let bytes = text.as_bytes();
|
||||
let mut seen = [false; 26];
|
||||
let mut unique = 0;
|
||||
for &b in bytes {
|
||||
let idx = (b - b'A') as usize;
|
||||
if !seen[idx] {
|
||||
seen[idx] = true;
|
||||
unique += 1;
|
||||
}
|
||||
}
|
||||
let variety = unique as f64 / bytes.len() as f64;
|
||||
score += variety * 0.1;
|
||||
|
||||
// bonus for common letters
|
||||
let common = b"ETAOINSHRDLU";
|
||||
let common_count = bytes.iter().filter(|b| common.contains(b)).count();
|
||||
let common_ratio = common_count as f64 / bytes.len() as f64;
|
||||
score += common_ratio * 0.05;
|
||||
|
||||
score
|
||||
}
|
||||
|
||||
pub fn is_related(a: &str, b: &str) -> bool {
|
||||
// one is a prefix of the other (SEAL / SEALS, RUN / RUNNING)
|
||||
if a.len() >= 3 && b.len() >= 3 {
|
||||
let (short, long) = if a.len() <= b.len() { (a, b) } else { (b, a) };
|
||||
if long.starts_with(short) {
|
||||
let suffix = &long[short.len()..];
|
||||
// common inflection suffixes
|
||||
if matches!(suffix, "S" | "ES" | "ED" | "ER" | "ING" | "LY" | "TION" | "NESS" | "MENT" | "ABLE" | "ISH" | "IST" | "IZE") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// handle e-dropping: MAKE/MAKING, BAKE/BAKING
|
||||
if short.len() >= 4 && long.ends_with("ING") {
|
||||
let stem = &long[..long.len() - 3];
|
||||
if short.len() == stem.len() + 1 && short.starts_with(stem) && short.ends_with('E') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// consonant doubling: RUN/RUNNING, SIT/SITTING
|
||||
if long.ends_with("ING") && long.len() >= 6 {
|
||||
let before_ing = &long[..long.len() - 3];
|
||||
let bb = before_ing.as_bytes();
|
||||
if bb.len() >= 4 && bb[bb.len() - 1] == bb[bb.len() - 2] {
|
||||
let stem = &before_ing[..before_ing.len() - 1];
|
||||
if short == stem {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// also ED doubling: STOP/STOPPED
|
||||
if long.ends_with("ED") && long.len() >= 5 {
|
||||
let before_ed = &long[..long.len() - 2];
|
||||
let bb = before_ed.as_bytes();
|
||||
if bb.len() >= 4 && bb[bb.len() - 1] == bb[bb.len() - 2] {
|
||||
let stem = &before_ed[..before_ed.len() - 1];
|
||||
if short == stem {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn related_words() {
|
||||
assert!(is_related("SEAL", "SEALS"));
|
||||
assert!(is_related("RUN", "RUNNING"));
|
||||
assert!(is_related("MAKE", "MAKING"));
|
||||
assert!(is_related("PLAY", "PLAYED"));
|
||||
assert!(is_related("PLAY", "PLAYER"));
|
||||
assert!(is_related("QUICK", "QUICKLY"));
|
||||
assert!(!is_related("CAT", "DOG"));
|
||||
assert!(!is_related("SEA", "SEAL"));
|
||||
assert!(!is_related("THE", "THEM"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user