import collections import random def is_alphabet(s): return all('a' <= ch <= 'z' for ch in s.strip().lower()) class Problem: def __init__(self, answer=None): if answer is not None: self.answer = answer return with open("/usr/share/dict/words") as file: self.answer = random.choice([s.strip().lower() for s in file if len(s) == 6 and is_alphabet(s)]) def guess(self, word): if len(word) != 5 or not is_alphabet(word): raise ValueError(f"Unexpected guess: {word}") return "".join( ['C' if word[i] == self.answer[i] else 'E' if word[i] in self.answer else '.' for i in range(len(word))]) class Guesser: def __init__(self): with open("/usr/share/dict/words") as file: self.possible_answers = [s.strip().lower() for s in file if len(s) == 6 and is_alphabet(s)] self.possible_guesses = self.possible_answers.copy() self.round = 0 def think(self, guess, result): """Update possible answers based on the guess and the result. """ if result == "CCCCC": return self.possible_guesses.remove(guess) for i in range(len(result)): ch = guess[i] res = result[i] if res == ".": self.possible_answers = list(filter(lambda word: ch not in word, self.possible_answers)) elif res == "E": self.possible_answers = list(filter(lambda word: ch in word and word[i] != ch, self.possible_answers)) else: self.possible_answers = list(filter(lambda word: word[i] == ch, self.possible_answers)) def guess(self): self.round += 1 if self.round == 1: return "oates" # Most efficient guess for the 1st time if len(self.possible_answers) == 1: return self.possible_answers[0] def bonus(word): return 1 if word in self.possible_answers else 0 return max(self.possible_guesses, key=lambda guess: self.score(guess) + bonus(guess)) def score(self, guess): """Score a guess from a point of view how much variety of hints (e.g., CCC..EE) it would produce. """ def hint(word, answer): return "".join( ['C' if word[i] == answer[i] else 'E' if word[i] in answer else '.' for i in range(len(word))]) # Compute frequency of hints a guess would produce counter = collections.Counter() for a in self.possible_answers: counter[hint(guess, a)] += 1 # Lower the score if many possible answers would produce the same hint. # In other words, the score is how much likely the guess results in various hints. # If we want to be more accurate here, we are able to compute how much this guess could reduce the entropy. return len(self.possible_answers) - counter.most_common(1)[0][1] class Round: def __init__(self, answer=None): self.problem = Problem(answer) self.guesser = Guesser() self.round = 0 self.history = [] def play_round(self): self.round += 1 word = self.guesser.guess() result = self.problem.guess(word) self.history.append((word, result)) # print(f"Round {self.round}: Guess \"{word}\" -> {result}") self.guesser.think(word, result) if result == "CCCCC": return True else: return False def play(self): while self.round < 6: result = self.play_round() if result: return True, self.round return False, self.round def random_mode(): N = 50 num_solved = 0 for _ in range(N): r = Round() (res, num_round) = r.play() if res: num_solved += 1 print(f"Congratulations! You solved the problem in {num_round} round(s).") else: print(f"Failed to solve the problem. The answer was \"{r.problem.answer}\".") print() print(f"Number of correct answer: {num_solved} / {N}") print(f"Correct answer rate: {num_solved * 100.0 / N}%") def exhaustive_mode(file=None): if file is None: file = "all.txt" with open(file) as f: problems = [s.strip().lower() for s in f if len(s) == 6 and is_alphabet(s)] num_solved = 0 for p in problems: r = Round(p) (res, num_round) = r.play() if res: num_solved += 1 else: for h in r.history: print(f"\"{h[0]}\" -> {h[1]}") print(f"Answer was \"{p}\"") print() print(f"Number of correct answer: {num_solved} / {len(problems)}") print(f"Correct answer rate: {num_solved * 100.0 / len(problems)}%") def interactive_mode(): guesser = Guesser() for _ in range(6): guess = guesser.guess() print(f"Guess: {guess}") result = input("Input the result in C/E/.: ").strip() if result == "CCCCC": break guesser.think(guess, result) if __name__ == '__main__': # random_mode() exhaustive_mode() # interactive_mode()