Source code for src.modules.backend.solver.stateless_entropy_strategy

# src/modules/backend/solver/stateless_entropy_strategy.py
"""
Stateless entropy-based solver strategy for Wordle using information theory principles.
"""
from typing import TYPE_CHECKING, List, Optional, Set, Tuple

from .entropy_mixin import EntropyCalculationMixin
from .stateless_solver_strategy import StatelessSolverStrategy

if TYPE_CHECKING:
    from ..legacy_word_manager import WordManager
    from ..stateless_word_manager import StatelessWordManager


[docs] class StatelessEntropyStrategy(StatelessSolverStrategy, EntropyCalculationMixin): """Stateless strategy that uses information theory to maximize information gain."""
[docs] def get_top_suggestions( self, constraints: List[Tuple[str, str]], count: int = 10, word_manager: Optional["WordManager"] = None, stateless_word_manager: Optional["StatelessWordManager"] = None, prefer_common: bool = True, word_set: Optional[Set[str]] = None, ) -> List[str]: """Get top N suggestions based on entropy (information gain) using stateless filtering.""" # Get filtered words using stateless methods possible_words, common_words = self._get_filtered_words( constraints, word_manager, stateless_word_manager, word_set ) if not possible_words: return [] if len(possible_words) <= count: # Use common utility for sorting by commonness return self._sort_by_entropy_and_commonness( possible_words, common_words, word_manager, stateless_word_manager, prefer_common, ) # For the first guess (no constraints), use predefined strong starters if not constraints: return self._get_good_starters( possible_words, common_words, count, word_manager, stateless_word_manager, ) # Use memory-optimized candidate selection candidates_to_evaluate = self._get_limited_candidates( possible_words, common_words, max_possible=50, max_additional_common=10 ) # Calculate entropy scores for candidates def entropy_scoring_func(word: str) -> float: # Use pre-calculated entropy if available precalc_entropy = self._get_word_entropy( word, word_manager, stateless_word_manager ) if precalc_entropy > 0: return float(precalc_entropy) # Fallback to calculated entropy using shared mixin method return float(self._calculate_entropy_optimized(word, possible_words)) # Use generator-based processing to reduce memory usage word_scores = [ (word, entropy_scoring_func(word)) for word in candidates_to_evaluate ] word_scores.sort(key=lambda x: x[1], reverse=True) # Get top candidates top_candidates = [word for word, _ in word_scores[: count * 2]] # Handle late game optimization if len(possible_words) <= 5: # In late game, favor words that could be the answer possible_matches = [w for w in top_candidates if w in possible_words] other_matches = [w for w in top_candidates if w not in possible_words] return (possible_matches + other_matches)[:count] # Balance common and other words if prefer_common is True if prefer_common: return self._balance_common_and_other(top_candidates, common_words, count) else: return top_candidates[:count]
def _get_good_starters( self, possible_words: List[str], common_words: List[str], count: int, word_manager: Optional["WordManager"] = None, stateless_word_manager: Optional["StatelessWordManager"] = None, ) -> List[str]: """Get good starting words based on entropy.""" # Predefined strong starters with high entropy strong_starters = [ "SLATE", "CRANE", "ADIEU", "AUDIO", "RAISE", "TEARS", "ROATE", ] # Filter starters that are available in the word set available_starters = [ word for word in strong_starters if word in possible_words ] if len(available_starters) >= count: return available_starters[:count] # If not enough predefined starters, add more based on entropy remaining_words = [w for w in possible_words if w not in available_starters] # Sort remaining words by entropy remaining_with_entropy = [ (word, self._get_word_entropy(word, word_manager, stateless_word_manager)) for word in remaining_words[:100] # Limit for performance ] remaining_with_entropy.sort(key=lambda x: x[1], reverse=True) additional_words = [ word for word, _ in remaining_with_entropy[: count - len(available_starters)] ] return available_starters + additional_words def _get_limited_candidates( self, possible_words: List[str], common_words: List[str], max_possible: int = 50, max_additional_common: int = 10, ) -> List[str]: """Get a limited set of candidates for evaluation to improve performance.""" # Start with possible words (limited) candidates = possible_words[:max_possible] # Add common words that aren't already included existing_candidates = set(candidates) additional_common = [ word for word in common_words if word not in existing_candidates ][: max_possible // 3] return candidates + additional_common def _sort_by_entropy_and_commonness( self, words: List[str], common_words: List[str], word_manager: Optional["WordManager"] = None, stateless_word_manager: Optional["StatelessWordManager"] = None, prefer_common: bool = True, ) -> List[str]: """Sort words by entropy with optional common word priority.""" if not prefer_common: # Simple entropy sort return sorted( words, key=lambda w: self._get_word_entropy( w, word_manager, stateless_word_manager ), reverse=True, ) # Separate common and non-common words common_set = set(common_words) common_words_filtered = [w for w in words if w in common_set] other_words = [w for w in words if w not in common_set] # Sort each group by entropy common_sorted = sorted( common_words_filtered, key=lambda w: self._get_word_entropy( w, word_manager, stateless_word_manager ), reverse=True, ) other_sorted = sorted( other_words, key=lambda w: self._get_word_entropy( w, word_manager, stateless_word_manager ), reverse=True, ) # Combine with common words first return common_sorted + other_sorted def _balance_common_and_other( self, candidates: List[str], common_words: List[str], count: int ) -> List[str]: """Balance common and other words in the final suggestions.""" common_set = set(common_words) common_candidates = [w for w in candidates if w in common_set] other_candidates = [w for w in candidates if w not in common_set] # Aim for roughly 60% common words, 40% other words common_target = max(1, int(count * 0.6)) other_target = count - common_target # Take up to target from each group result = [] result.extend(common_candidates[:common_target]) result.extend(other_candidates[:other_target]) # Fill remaining slots with best candidates overall remaining_slots = count - len(result) if remaining_slots > 0: remaining_candidates = [w for w in candidates if w not in result] result.extend(remaining_candidates[:remaining_slots]) return result[:count]