Решатель Wordgame на Python в 270 строках

Живая версия у меня github

Lingo — это игра, в которой ведущий тайно выбирает слово из 5 букв, а затем предоставляет игроку первую букву. Затем игрок угадывает слово, а ведущий сообщает, какие буквы правильные, неправильные или неправильные.

Я называю эту обратную связь match_string и использую следующий формат:

‘s’ = правая буква, правая позиция (для обозначения квадрата)

‘o’ = правильная буква, неправильное положение (для обозначения круга)

‘x’ = буквы нет в слове. (чтобы представить .. ну .. X)

Это читер для Lingo. Он загружает данные из списка слов (в данном случае из словаря с поиском словаря), чтобы найти потенциальные слова. Он также дает предположения как по вероятности наличия символов в оставшихся словах, так и по частоте использования английского слова.

from collections import defaultdict, Counter
from copy import copy
from math import log
from random import choices
import re
import os
import pickle

# VARIABLES

num_loops = 10**4

word_len = 5
max_guesses = 5

word_list="Collins Scrabble Words (2019).txt"
freq_list="all.num.o5.txt"
cache_file="cached.pickle"

# PROGRAM


def main():
    wl = WordList()

    # TODO: add a mode where human can play vs comp supplied words
    print("""
1. [H]uman enters guesses and match strings from an external source
2. [C]omputer plays vs itself""")
    while True:
        i = input('Choice?').lower()

        if i in ['1', 'h']:
            human_player(wl)
        elif i in ['2', 'c', '']:
            CompPlay(wl).cp_main()
            break
        else:
            print('Invalid Choice')


def human_player(wl):
    while True:
        first_letter = input('What's the first letter?').upper()

        pc = PossCalculator(wl, first_letter)

        while True:
            pc.print_best(5)

            guess = input('Guess?').upper()
            if guess == '':
                guess = first_letter + pc.get_best(1)[0][0]
                print(f'Guessing: {guess}')
            elif guess[1:] not in pc.poss:
                print(guess, 'is not a valid word. Please try again')
                continue

            match_string = input('Match String?').lower()
            if not re.search(r'[sox]{'+str(word_len)+'}', match_string):
                print('invalid match string. Please try again')

            num_poss = pc.calc_matches(guess, match_string)

            if num_poss == 1:
                print(f'  -={guess}=-')
                break

            print(f'  {num_poss} words left')

            if num_poss == 0:
                print('  WTF did you do?')
                break


def str_pos_sub(string, pos, sub):
    return string[:pos] + sub + string[pos + 1:]


class CompPlay:
    def __init__(self, wl):
        self.wl = wl

    def cp_main(self):
        guess_counter = Counter()
        for _ in range(num_loops):
            word = self.get_word()[0]
            print(f'Word is: {word}')
            pc = PossCalculator(self.wl, word[0])

            guesses = []
            while True:
                guess = word[0] + pc.get_best(1)[0][0]
                if guess in guesses:
                    pc.poss.discard(guess[1:])
                    continue

                guesses.append(guess)


                if len(guesses) > max_guesses:
                    print('  :( too many guesses')
                    guess_counter['DQ'] += 1
                    break
                elif guess == word:
                    print(f'  -={word}=-')
                    print(f'   {len(guesses)} guesses')
                    guess_counter[len(guesses)] += 1
                    break

                match_string = self.get_match_string(word, guess)
                num_poss = pc.calc_matches(guess, match_string)

                print(f'    {guess}t{match_string}t{num_poss} words left')

                if word[1:] not in pc.poss:
                    print('  WTF did you do?')
                    guess_counter['WTF'] += 1
                    break

        print('n')
        for guesses, count in guess_counter.most_common():
            print(f'{count:5d} solved in {guesses} guesses')

    def get_match_string(self, word, guess):
        match_string = '.' * word_len
        for pos in range(word_len):
            if guess[pos] == word[pos]:
                match_string = str_pos_sub(match_string, pos, 's')
                word = word.replace(word[pos], '.', 1)

        for pos in range(word_len):
            if match_string[pos] != '.':
                continue
            elif guess[pos] in word[1:]:
                match_string = str_pos_sub(match_string, pos, 'o')
                word = word.replace(guess[pos], '.', 1)
            else:
                match_string = str_pos_sub(match_string, pos, 'x')

        return match_string

    def get_word(self):
        return choices(
            list(self.wl.word_freq.keys()),  # population
            list(self.wl.word_freq.values()),  # weights  # TODO: speedup by turning this into a cached cumulative list
        )


class PossCalculator:
    def __init__(self, wl, first_letter):
        self.wl = wl
        self.first_letter = first_letter
        self.poss = copy(wl.starts_with(first_letter))
        print(f' starting letter {first_letter}, {len(self.poss)} words left')

    def calc_matches(self, guess, match_string):
        guess = guess[1:]
        match_string = match_string[1:]

        poss_copy = copy(self.poss)
        for word in poss_copy:
            if not self.check_valid(guess, match_string, word):
                self.poss.remove(word)

        return len(self.poss)

    def check_valid(self, guess, match_string, word):
        pos_dict = {
            's': [],
            'o': [],
            'x': [],
        }

        for pos, char in enumerate(match_string):
            pos_dict[char].append(pos)

        for pos in pos_dict['s']:
            if guess[pos] == word[pos]:
                word = str_pos_sub(word, pos, '.')
            else:
                return False

        for pos in pos_dict['o']:
            if guess[pos] in word and guess[pos] != word[pos]:
                word = word.replace(guess[pos], '.', 1)
            else:
                return False

        for pos in pos_dict['x']:
            if guess[pos] in word:
                return False

        # You have passed the three trials of the match_string. You have proven yourself.
        return True

    def get_best(self, n):
        char_score = Counter()
        for word in self.poss:
            for char in set(word):
                char_score[char] += 1

        word_scores = Counter()
        for word in self.poss:
            word_set = set(word)
            for char in word_set:
                word_scores[word] += char_score[char]

            word_scores[word] *= (len(word_set) + 1)

        avg_word_score = int(sum(word_scores.values()) / len(word_scores))

        for word, score in word_scores.items():
            word_scores[word] = int(score / avg_word_score * 130)
            word_scores[word] += self.wl.word_freq[self.first_letter + word]

        return word_scores.most_common(n)

    def print_best(self, n):
        for word, score in self.get_best(n):
            print(f'{self.first_letter}{word}t{score}')


class WordList:
    def __init__(self):
        if os.path.exists(cache_file):
            print('Loading cached wordlist!')  # TODO: pickle doesn't want to dump the variables, see below
            # with open(cache_file, 'rb') as f:
            #     self.word_dict = pickle.load(f)
            #     self.word_freq = pickle.load(f)
        else:
            print('Building wordlist!')
            self.build_wordlists()


    def build_wordlists(self):

        self.word_dict = defaultdict(set)
        # word_dict is {first_letter: [rest_of_word1, rest_of_word2]}

        with open(word_list) as f:
            for word in f:
                if len(word) == word_len+1:
                    self.word_dict[word[0]].add(word[1:word_len])
                    # we already know the first letter, so cut off with [1:]
                    # there's a newline while reading, so cut it off with [:5]

        self.word_freq = defaultdict(lambda: 40)

        with open(freq_list) as f:
            for line in f:
                line = line.split()
                if len(line[1]) == word_len:
                    word = line[1].upper()
                    if word[1:] in self.word_dict[word[0]]:
                        self.word_freq[word] = int(log(int(line[0]), 6) * 40)

        for word in self.word_freq:
            assert word[1:] in self.word_dict[word[0]]

        # with open(cache_file, 'wb') as f:
        #     pickle.dump((self.word_dict, self.word_freq), f)

    def starts_with(self, first_letter):
        return self.word_dict[first_letter]


if __name__ == '__main__':
    main()

1 ответ
1

Качество вашего кода хорошее. У вас есть четкий последовательный стиль, который выглядит вполне совместимым с PEP 8. Единственное вопиющее изменение, которое я внесу, это то, что ваши переменные в верхней части файла на самом деле являются константами. Таким образом, имена будут UPPER_SNAKE_CASE если вы решите следовать PEP 8 здесь.

Если мы посмотрим на отдельные строки или функции кода отдельно, ваш код будет довольно сильным. У вас не будет проблем с глазурованием при использовании плохих шаблонов «линейного уровня».

Однако код трудно читать и понимать. Я думаю, вы сосредоточились на том, чтобы обман работал, поэтому вы не особо задумывались над общей структурой. Например, у вас есть повторяющаяся логика в human_player а также CompPlay.cp_main. Вы можете создать общий интерфейс между двумя методами игры.

Давайте посмотрим, как мы могли бы создать общий интерфейс на основе описания в вашем вопросе.

  • choose_word

    Lingo — это игра, в которой ведущий тайно выбирает слово из 5 букв, а затем предоставляет игроку первую букву.

  • get_guess

    Затем игрок угадывает слово,

  • get_match

    и ведущий дает обратную связь о том, какие буквы правильные, неправильные или неправильные.

  • is_match

    Предположительно, мы хотим прекратить играть, как только угадаем правильное слово.

  • complete

    Хозяин поздравит победителей, можно здесь.

Вот пример реализации на Python.

from typing import Protocol


class IGame(Protocol):
    def choose_word(self) -> int: ...
    def get_guess(self, size: int) -> str: ...
    def get_match(self, guess: str) -> str: ...
    def is_match(self, guess: str, match: str) -> bool: ...
    def complete(self, guess: str) -> None: ...


class HumanGame:
    known_words: list[str]

    def __init__(self, known_words: list[str]) -> None:
        self.known_words = known_words

    def choose_word(self) -> int:
        self.word = random.choice(self.known_words)
        print(f"Computer has chosen a word starting with {self.word[0]}")
        return len(self.word)

    def get_guess(self, size: int) -> str:
        while True:
            guess = input(f"Guess a {size} letter word? ").upper()
            if len(guess) != size:
                print("Invalid length guess")
                continue
            break
        return guess

    def get_match(self, guess: str) -> str:
        characters = set(self.word)
        return [
            "s"
            if g == w else
            "o"
            if g in characters else
            "x"
            for g, w in zip(guess, self.word)
        ]

    def is_match(self, guess: str, match: str) -> bool:
        return match == "s" * len(guess)

    def complete(self, guess: str) -> None:
        print(f"You are correct, {guess} is the word.")


def game_main(game: IGame):
    size = game.choose_word()
    while True:
        guess = game.get_guess(size)
        match = game.get_match(guess)
        if game.is_match(guess, match):
            break
    game.complete()


def main():
    game_main(HumanGame(["CAT", "BAT", "WAT"]))


if __name__ == "__main__":
    main()

Отсюда мы можем сосредоточиться на выборе лучшего. Я считаю ваш код довольно сложным.

В коде есть два тесно связанных класса для выбора лучшего. Некоторые решения о том, как хранить данные, делают код довольно сложным. И в коде отсутствует применяемый принцип единой ответственности (SRP).

Давайте сосредоточимся только на word_list;

  • Я не могу передать список слов WordList. (нарушение функциональности)
  • Код читается из глобального word_list. (нарушение функциональности)
  • Класс связал частоту при построении списка слов. (повышенная сложность)
  • Вы можете только читать из файла сразу, применив свои фильтры. (нарушение возможности повторного использования)
  • Вы фильтруете вывод в PossCalculator. (повышенная сложность)

Давайте сосредоточимся на том, что вы делаете со списком слов:

  • Читать из файла.
  • Фильтруйте слова по длине.
  • Фильтровать слова по начальной букве.
    (Фактически вы группируете, а затем индексируете словарь, но функционально то же самое.)
  • Фильтровать по match_string а также guess.
  • Дайте каждому слову оценку по буквам.

Таким образом, мы можем создать один класс, который будет делать все.

from __future__ import annotations


class Words(list[str]):
    @classmethod
    def from_path(cls, path: str) -> Words:
        with open(path) as f:
            return cls([word.strip() for word in f])

    def filter_length(self, size: int) -> Words:
        return Words([
            word
            for word in self
            if len(word) == size
        ])

    def filter_start(self, start: str) -> Words:
        return Words([
            word
            for word in self
            if word.startswith(start)
        ])

    def filter_match(self, guess: str, match: str):
        return Words([
            word
            for word in self
            if self._check_valid(guess, match, word)
        ])

    @staticmethod
    def _check_valid(guess: str, match: str, word: str) -> bool:
        pos_dict = {
            's': [],
            'o': [],
            'x': [],
        }
        for pos, char in enumerate(match):
            pos_dict[char].append(pos)
        for pos in pos_dict['s']:
            if guess[pos] == word[pos]:
                word = word[:pos] + '.' + word[pos + 1:]
            else:
                return False
        for pos in pos_dict['o']:
            if guess[pos] in word and guess[pos] != word[pos]:
                word = word.replace(guess[pos], '.', 1)
            else:
                return False
        for pos in pos_dict['x']:
            if guess[pos] in word:
                return False
        return True

    def get_scores(self) -> collections.Counter[str, int]:
        char_score = collections.Counter()
        for word in self:
            for char in set(word):
                char_score[char] += 1
        word_scores = collections.Counter()
        for word in self:
            word_set = set(word)
            for char in word_set:
                word_scores[word] += char_score[char]
        return word_scores

Я перешел к грубой повторной реализации CompPlay с вышеуказанными изменениями. Итак, вы видите, как я использую код.
Примечание: все блоки кода не протестированы

from __future__ import annotations
import random
import math
import collections
from typing import Iterable, Protocol


class IGame(Protocol):
    def choose_word(self) -> int: ...
    def get_guess(self, size: int) -> str: ...
    def get_match(self, guess: str) -> str: ...
    def is_match(self, guess: str, match: str) -> bool: ...
    def complete(self, guess: str) -> None: ...


class HumanGame:
    known_words: list[str]

    def __init__(self, known_words: list[str]) -> None:
        self.known_words = known_words

    def choose_word(self) -> int:
        self.word = random.choice(self.known_words)
        print(f"Computer has chosen a word starting with {self.word[0]}")
        return len(self.word)

    def get_guess(self, size: int) -> str:
        while True:
            guess = input(f"Guess a {size} letter word? ").upper()
            if len(guess) != size:
                print("Invalid length guess")
                continue
            break
        return guess

    def get_match(self, guess: str) -> str:
        characters = set(self.word)
        return [
            "s"
            if g == w else
            "o"
            if g in characters else
            "x"
            for g, w in zip(guess, self.word)
        ]

    def is_match(self, guess: str, match: str) -> bool:
        return match == "s" * len(guess)

    def complete(self, guess: str) -> None:
        print(f"You are correct, {guess} is the word.")


def game_main(game: IGame):
    size = game.choose_word()
    while True:
        guess = game.get_guess(size)
        match = game.get_match(guess)
        if game.is_match(guess, match):
            break
    game.complete()


class Words(list[str]):
    @classmethod
    def from_path(cls, path: str) -> Words:
        with open(path) as f:
            return cls([word.strip() for word in f])

    def filter_length(self, size: int) -> Words:
        return Words([
            word
            for word in self
            if len(word) == size
        ])

    def filter_start(self, start: str) -> Words:
        return Words([
            word
            for word in self
            if word.startswith(start)
        ])

    def filter_match(self, guess: str, match: str):
        return Words([
            word
            for word in self
            if self._check_valid(guess, match, word)
        ])

    @staticmethod
    def _check_valid(guess: str, match: str, word: str) -> bool:
        pos_dict = {
            's': [],
            'o': [],
            'x': [],
        }
        for pos, char in enumerate(match):
            pos_dict[char].append(pos)
        for pos in pos_dict['s']:
            if guess[pos] == word[pos]:
                word = word[:pos] + '.' + word[pos + 1:]
            else:
                return False
        for pos in pos_dict['o']:
            if guess[pos] in word and guess[pos] != word[pos]:
                word = word.replace(guess[pos], '.', 1)
            else:
                return False
        for pos in pos_dict['x']:
            if guess[pos] in word:
                return False
        return True

    def get_scores(self) -> collections.Counter[str, int]:
        char_score = collections.Counter()
        for word in self:
            for char in set(word):
                char_score[char] += 1
        word_scores = collections.Counter()
        for word in self:
            word_set = set(word)
            for char in word_set:
                word_scores[word] += char_score[char]
        return word_scores


class Frequencies(dict[str, int]):
    @classmethod
    def from_frequencies(cls, frequencies: Iterable[tuple[int, str]]) -> Frequencies:
        return cls({
            word: int(math.log(frequency, 6) * 40)
            for frequency, word in frequencies
        })

    @classmethod
    def from_path(cls, path: str) -> Frequencies:
        with open(path) as f:
            return cls.from_frequencies(
                (int(split[0]), split[1])
                for line in f
                if (split := line.split())
            )


class PossCalculator:
    def __init__(self, words: Words, freqs: Frequencies) -> None:
        self.words = words
        self.freqs = freqs

    def calc_matches(self, guess: str, match: str) -> int:
        self.words = self.words.filter_match(guess, match)
        return len(self.words)

    def get_bests(self) -> collections.Counter[str, int]:
        word_scores = self.words.get_scores()
        avg_word_score = int(sum(word_scores.values()) / len(word_scores))
        for word, score in word_scores.items():
            word_scores[word] = int(score / avg_word_score * 130)
            word_scores[word] += self.freqs[word]
        return word_scores

    def get_best(self) -> str:
        return self.get_bests().most_common(1)[0][0]

    def print_best(self, n):
        for word, score in self.get_bests().most_common(n):
            print(f'{word}t{score}')


class ComputerGame:
    pos: PossCalculator
    word: str
    guesses: set[str]

    def __init__(self, pos: PossCalculator) -> None:
        self.pos = pos
        self.guesses = set()

    def choose_word(self) -> int:
        self.word = random.choices(
            list(self.pos.freqs.keys()),
            list(self.pos.freqs.values()),
        )[0]
        print(f'Word is: {self.word}')
        return len(self.word)

    def get_guess(self, size: int) -> str:
        while True:
            guess = self.pos.get_best()
            if guess in self.guesses:
                continue
            self.guesses.add(guess)
            return guess

    def get_match(self, guess: str) -> str:
        characters = set(self.word)
        return [
            "s"
            if g == w else
            "o"
            if g in characters else
            "x"
            for g, w in zip(guess, self.word)
        ]

    def is_match(self, guess: str, match: str) -> bool:
        return match == "s" * len(guess)

    def complete(self, guess: str) -> None:
        print(f'{self.word} solved in {len(self.guesses)} guesses')


def main():
    WORD_LIST = 'Collins Scrabble Words (2019).txt'
    FREQ_LIST = 'all.num.o5.txt'
    words = Words.from_path(WORD_LIST).filter_length(5)
    frequencies = Frequencies.from_path(FREQ_LIST)
    game_main(ComputerGame(PossCalculator(words, frequencies)))


if __name__ == "__main__":
    main()

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *