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")); } }