Evaluate position based on remaining moves
This commit is contained in:
106
src/wordle.py
106
src/wordle.py
@@ -38,8 +38,16 @@ class WordleState:
|
|||||||
)
|
)
|
||||||
|
|
||||||
class DictionaryProvider:
|
class DictionaryProvider:
|
||||||
def provide(self) -> List[Word]:
|
|
||||||
return self._get_dictionary(self._word_filter(WORD_LENGTH))
|
def __init__(self, limit: Optional[int]=None):
|
||||||
|
self.limit = limit
|
||||||
|
|
||||||
|
def provide(self, ) -> List[Word]:
|
||||||
|
words = self._get_dictionary(self._word_filter(WORD_LENGTH))
|
||||||
|
if self.limit:
|
||||||
|
return words[:self.limit]
|
||||||
|
else:
|
||||||
|
return words
|
||||||
|
|
||||||
def _get_dictionary(self, filter_fun: Callable[str, bool] = None) -> List[Word]:
|
def _get_dictionary(self, filter_fun: Callable[str, bool] = None) -> List[Word]:
|
||||||
l = []
|
l = []
|
||||||
@@ -63,13 +71,36 @@ class DictionaryProvider:
|
|||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
class WordleStateEvaluator:
|
class WordleWordPlayable:
|
||||||
|
|
||||||
|
def is_word_playable(self, state: WordleState, word: Word):
|
||||||
|
for idx, ch in enumerate(word):
|
||||||
|
# Position is not already solved
|
||||||
|
if state.board[idx] is not None and state.board[idx] != ch:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Letter is not already excluded
|
||||||
|
constraint = state.letter_constraints[ch]
|
||||||
|
if constraint == WordleLetterConstraint.EXCLUDED:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if ch in state.letters_eliminated[idx]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for letter, constraint in state.letter_constraints.items():
|
||||||
|
if constraint == WordleLetterConstraint.INCLUDED and letter not in word:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
class WordleRemainingWordsStateEvaluator:
|
||||||
"""
|
"""
|
||||||
A class which takes a WordleState and returns a score for the state
|
A class which takes a WordleState and returns a score for the state
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, dictionary_provider: DictionaryProvider):
|
def __init__(self, dictionary_provider: DictionaryProvider, word_playable: WordleWordPlayable):
|
||||||
self.dictionary_provider = dictionary_provider
|
self.dictionary = dictionary_provider.provide()
|
||||||
|
self.word_playable = word_playable
|
||||||
|
|
||||||
def evaluate(self, state: WordleState) -> int:
|
def evaluate(self, state: WordleState) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -77,29 +108,7 @@ class WordleStateEvaluator:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Score values are totally arbitrary. This will be improved in the future based on the remaining selections
|
# Score values are totally arbitrary. This will be improved in the future based on the remaining selections
|
||||||
|
return -sum([1 for word in self.dictionary if self.word_playable.is_word_playable(state, word)])
|
||||||
# Game is solved. Return a big number.
|
|
||||||
if all(state.board):
|
|
||||||
return 999999999
|
|
||||||
|
|
||||||
score = 0
|
|
||||||
for constraint in state.letter_constraints.values():
|
|
||||||
if constraint == WordleLetterConstraint.UNKNOWN:
|
|
||||||
score += 0
|
|
||||||
elif constraint == WordleLetterConstraint.EXCLUDED:
|
|
||||||
score += 10
|
|
||||||
elif constraint == WordleLetterConstraint.INCLUDED:
|
|
||||||
score += 100
|
|
||||||
|
|
||||||
for letter in state.board:
|
|
||||||
if letter:
|
|
||||||
score += 100
|
|
||||||
|
|
||||||
for letters in state.letters_eliminated:
|
|
||||||
score += len(letters)
|
|
||||||
|
|
||||||
return score
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WordleMoveProcessor:
|
class WordleMoveProcessor:
|
||||||
@@ -133,19 +142,21 @@ class WordleMoveCalculator:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
dictionary_provider: DictionaryProvider,
|
dictionary_provider: DictionaryProvider,
|
||||||
state_evaluator: WordleStateEvaluator,
|
state_evaluator, # TODO: parent class WordleStateEvaluator,
|
||||||
move_processor: WordleMoveProcessor,
|
move_processor: WordleMoveProcessor,
|
||||||
|
word_playable: WordleWordPlayable,
|
||||||
):
|
):
|
||||||
self.dictionary = dictionary_provider.provide()
|
self.dictionary = dictionary_provider.provide()
|
||||||
self.state_evaluator = state_evaluator
|
self.state_evaluator = state_evaluator
|
||||||
self.move_processor = move_processor
|
self.move_processor = move_processor
|
||||||
|
self.word_playable = word_playable
|
||||||
|
|
||||||
def calculate(
|
def calculate(
|
||||||
self, state: WordleState, evaluation_depth: int, limit: Optional[int]=None,
|
self, state: WordleState, evaluation_depth: int, limit: Optional[int]=None,
|
||||||
) -> List[Tuple[Word, int]]:
|
) -> List[Tuple[Word, int]]:
|
||||||
guesses = []
|
guesses = []
|
||||||
words = [
|
words = [
|
||||||
word for word in self.dictionary if self._is_word_playable(state, word)
|
word for word in self.dictionary if self.word_playable.is_word_playable(state, word)
|
||||||
]
|
]
|
||||||
|
|
||||||
move_rankings = [] # type: List[Tuple[Word, int]]
|
move_rankings = [] # type: List[Tuple[Word, int]]
|
||||||
@@ -169,36 +180,17 @@ class WordleMoveCalculator:
|
|||||||
move_rankings = move_rankings[0:limit]
|
move_rankings = move_rankings[0:limit]
|
||||||
return move_rankings
|
return move_rankings
|
||||||
|
|
||||||
def _is_word_playable(self, state: WordleState, word: Word):
|
|
||||||
for idx, ch in enumerate(word):
|
|
||||||
# Position is not already solved
|
|
||||||
if state.board[idx] is not None and state.board[idx] != ch:
|
|
||||||
return False
|
|
||||||
constraint = state.letter_constraints[ch]
|
|
||||||
|
|
||||||
# Letter is not already excluded
|
|
||||||
if constraint == WordleLetterConstraint.EXCLUDED:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if ch in state.letters_eliminated[idx]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for letter, constraint in state.letter_constraints.items():
|
|
||||||
if constraint == WordleLetterConstraint.INCLUDED and letter not in word:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
|
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
|
||||||
|
|
||||||
dictionary_provider = DictionaryProvider()
|
dictionary_provider = DictionaryProvider(limit=100)
|
||||||
state_evaluator = WordleStateEvaluator(dictionary_provider)
|
word_playable = WordleWordPlayable()
|
||||||
|
state_evaluator = WordleRemainingWordsStateEvaluator(dictionary_provider, word_playable)
|
||||||
move_processor = WordleMoveProcessor()
|
move_processor = WordleMoveProcessor()
|
||||||
move_calculator = WordleMoveCalculator(
|
move_calculator = WordleMoveCalculator(
|
||||||
dictionary_provider, state_evaluator, move_processor
|
dictionary_provider, state_evaluator, move_processor, word_playable
|
||||||
)
|
)
|
||||||
|
|
||||||
answer = random.choice(dictionary_provider.provide())
|
answer = random.choice(dictionary_provider.provide())
|
||||||
@@ -208,11 +200,9 @@ def main():
|
|||||||
while not all(state.board):
|
while not all(state.board):
|
||||||
log.info("Round: %d", iteration)
|
log.info("Round: %d", iteration)
|
||||||
log.debug("State: %s", str(state))
|
log.debug("State: %s", str(state))
|
||||||
if iteration == 1:
|
moves = move_calculator.calculate(state, 1)
|
||||||
guess = "sales"
|
log.info("Best Moves: %s", moves[:5])
|
||||||
else:
|
log.info("Num words remaining: %d", len(moves))
|
||||||
moves = move_calculator.calculate(state, 5, limit=5)
|
|
||||||
log.info("Best Moves: %s", moves)
|
|
||||||
guess = moves[0][0]
|
guess = moves[0][0]
|
||||||
log.info("Guess: %s", guess)
|
log.info("Guess: %s", guess)
|
||||||
state = move_processor.process(state, guess)
|
state = move_processor.process(state, guess)
|
||||||
|
|||||||
Reference in New Issue
Block a user