Живая версия у меня 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 ответ
Качество вашего кода хорошее. У вас есть четкий последовательный стиль, который выглядит вполне совместимым с 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()