My friend and I are kind of noobs in Python and are looking for ways to improve. We thought it would be cool if we could get some feedback on the following code. It is for a game named "Lingo", which is popular on TV in Europe.
Lingo is a popular word-guessing game show on television. The number of letters of a target word to be guessed is given, and often also the first letter. Players then make guesses subject to these restrictions (number of letters and possibly also first letter), and the game tells them which letters are correct and in the correct place, marked by a red square (X), and which letters are correct but not in the correct place, marked by a yellow circle (O). We do not use superfluous yellow circles, i.e. a letter is marked correct at most as often as it appears in the target word. If not all occurrences of the same letter can get a yellow circle this way, priority is given from left to right (but of course red squares have priority over yellow circles).
We wrote the following code with the following purpose:
First we needed to create a function compare that compares a guessed word with a target word. The two inputs are string of the same length that entirely consist of lowercase ASCII letters. The output is a string of the same length consisting of the symbols X, O and -, where X represent a red square, O represents a yellow circle and - represents nothing.
Examples:
compare("health", "teethe") must return "OX--O-",
compare("rhythm", "teethe") must return "---XX-",
compare("mutate", "teethe") must return "--O-OX",
compare("teethe", "mutate") must return "O--O-X",
Now we must solve for words of length-n, there are time constraints on how many steps it should take for our program to successfully find the right word, on average.
Constraints:
wordlist is a list of words (i.e. a list of strings that entirely consist of lowercase letters) you are allowed to guess. For example, all Dutch 7-letter words that start with "c".
targets is a list of possible target words, based on previous guesses. When no guesses have been made yet, targets will be equal to wordlist. In any case, all words in targets are guaranteed to be in wordlist; you don't have to check this.
The return value is a word (i.e. string) that appears in wordlist. It is supposed to be a smart guess that helps to find the actual target in very few turns. Often this guess is in the target list, but it can sometimes be smart to guess a word that is not a possible target, because it has more possibilities to eliminate other words. To give an example, let dw5s be the list of all Dutch 5-letter words that start with an s.Then smart_guess(dw5s, dw5s) should return a good first word to guess. There are many good words to guess at this stage; an example of what this function could return is 'stoel'. If this guess has 'XO-OO' as compare result, then the target list is narrowed down to ['sealt', 'slegt', 'smelt', 'snelt', 'spelt'], meaning that these are the only possible words left based on our guess. If we now call smart_guess(dw5s, ['sealt', 'slegt', 'smelt', 'snelt','spelt']), we will get a smart guess for the new target list. A possible return value is 'slemp'. If this guess has 'XOX-O' as compare result, the target list will be narrowed down further to ['spelt']. So 'spelt' is the word to be guessed, and smart_guess(dw5s,['spelt']) should return 'spelt'.
○ For 5-letter words, the target must be guessed correctly within 4 turns on average.
○ For 6-letter words, the target must be guessed correctly within 3.5 turns on average.
○ And for 7- or 8- letter words, it must be guessed correctly within 3 turns on average.
- Only imports from the standard library are allowed.
Another constraint:
If wordlist contains all Dutch words of a given length of at most 10 with a given starting letter (like in the TV show) and targets is any sublist thereof, the function call smart_guess(wordlist, targets) must finish within one second on a reasonably up-to-date computer. Again, timings will be tested on a 1.4 GHz i5 cpu, and you will have to adjust the timing requirement accordingly in case your computer speed differs wildly.
This is the wordlist we use: https://raw.githubusercontent.com/OpenTaal/opentaal-wordlist/master/wordlist.txt
import time
import random
def load_words(file):
result = set()
with open(file) as f:
for line in f.readlines():
word = line.strip().lower()
if word.isalpha() and word.isascii():
result.add(word)
return sorted(result)
english_words = load_words("words.txt")
dutch_words = load_words("wordlist.txt")
def compare(guess, target):
''' Compare two words and give string with 'X' letter is in good place, 'O' not in good place but in word and '-': not in the word. '''
result = list(target)
index_list = list(range(len(guess)))
letter_dict = {}
for letter in target:
letter_dict[letter] = target.count(letter)
# Iterate list of indexes
for idx in range(len(index_list)):
# Look which letters are in good place
if guess[idx] == target[idx]:
# Decrease letter count
letter_dict[guess[idx]] = letter_dict[guess[idx]] - 1
# Delete index from list add 'X'
result[idx] = "X"
index_list.remove(idx)
for idx in index_list:
#Check if letter still is in letter_dict and in target
if guess[idx] in target and letter_dict[guess[idx]] > 0:
# Remove lettercount from dict
letter_dict[guess[idx]] = letter_dict[guess[idx]] - 1
# Add 'O' to place in guess_list
result[idx] = "O"
else:
result[idx] = "-"
return "".join(result)
def filter_targets(targets, guess_results):
''' Compare every result of targets with potential targets in wordlist and return list with potential answers '''
end_targets = []
for target in targets:
#Create list with compared results
temp_list = []
for guess in guess_results:
temp_list.append(compare(guess, target))
#Compare results are the same, add to end_targets
if temp_list == list(guess_results.values()):
end_targets.append(target)
return end_targets
def distribution(guess, targets):
''' Return dictionairy with distribution of compared results, how good the guess is. '''
distribution_dict = {}
#Check how many times compared gives result
for target in targets:
result = compare(guess, target)
if result not in list(distribution_dict.keys()):
distribution_dict[result] = 1
else:
distribution_dict[result] += 1
return distribution_dict
def smart_guess(wordlist, targets):
''' Returns best guess after comparing the distributions of each sampled guess '''
#Get randomized sample from targetlist
samples = sample_targets(targets)
#Get big number to start with
min_largest_value = len(wordlist)
best_guess = ""
#Iterate trough samples
for guess in samples:
#Find the biggest number in distribution
biggest_value_in_distr = max(distribution(guess, targets).values())
#Check if biggest number is the smallest of all, if so, add the guess to best_guess
if biggest_value_in_distr < min_largest_value:
min_largest_value = biggest_value_in_distr
best_guess = guess
if min_largest_value <= 2:
return best_guess
return best_guess
def sample_targets(targets):
#Get randomized sample from targetlist and add a total random word
len_word = len(targets[0])
decr = 10
if len_word == 4:
sample_size = 100
decr -= 1
if len_word == 5:
sample_size = 100
decr -= 1
if len_word == 6:
sample_size = len_word * decr
decr -= 1
if len_word == 7:
sample_size = 60
decr -= 1
if len_word == 8:
sample_size = len_word * decr
decr -= 1
if len_word == 9:
sample_size = 8
decr -= 1
if len_word == 10:
sample_size = 5
#Need to find a way to pop these samples
# samples = sample(targets, sample_size)
samples = set([i for i in targets[0:sample_size]])
#Pick 5 random items out of list
# for _ in range(5):
# random.shuffle(targets)
# samples.append(targets.pop())
## NOT PERFECT YET, NEEDS TO POP ITEM
samples.add(random.choice(targets))
samples.add(random.choice(targets))
samples.add(random.choice(targets))
return samples
def simulate_game(target, wordlist):
n = len(target)
wordlist = [w for w in wordlist if len(w) == n and w[0] == target[0]]
if target not in wordlist:
raise ValueError("Target is not in wordlist, thus impossible to guess.")
targets = wordlist.copy()
turns = 0
while True:
num_words = len(targets)
print(f"There {'is' if num_words==1 else 'are'} {num_words} possible"
f" target{'s' if num_words!=1 else ''} left.")
turns += 1
guess = smart_guess(wordlist, targets)
if guess == str(guess):
print("My guess is: ", guess.upper())
result = compare(guess, target)
print("Correctness: ", result)
if result == n * "X":
print(f"Target was guessed in {turns} "
f"turn{'s' if turns!=1 else ''}.")
break
else:
targets = filter_targets(targets, {guess: result})
def count_turns(target, wordlist, runs):
n = len(target)
wordlist = [w for w in wordlist if len(w) == n and w[0]==target[0]]
targets = wordlist.copy()
global average_time
turns = 0
while True:
turns += 1
if turns > 100:
raise RuntimeError("This is going nowhere: 100 turns used.")
t0 = time.time()
guess = smart_guess(wordlist, targets)
t1 = time.time()
average_time += t1 - t0
result = compare(guess, target)
if result == n * "X":
break
else:
targets = filter_targets(targets, {guess: result})
return turns
def turn_count_simulation(word_length, wordlist, runs=100):
wordlist = [word for word in wordlist if len(word) == word_length]
total = 0
for _ in range(runs):
target = random.choice(wordlist)
total += count_turns(target, wordlist, runs)
return total/runs
testcases = [4, 5, 6, 7, 8, 9, 10]
runs = 100
for item in testcases:
print(f"Calculating {runs} runs for {item} letter words...")
result = turn_count_simulation(item, dutch_words, runs)
print(f"Averaged out at: {result}")
print("Average time taken: ", average_time / runs)
We are looking for ways in which we can probabilistic optimize our code and we also wonder if we are better of using sets or lists on some occasions. We are open for any suggestions that improve the speed of our program, this has been a great learning experience but we find it very hard to arrive at new solutions independently after working on this problem for a few weeks.
smart_guess(dw5s,'stoel')
returns'XO-OO'
, this information is not available for use in futuresmart_guess
calls. For example, this result indicates "there is a 'T', 'E', and 'L', but there is no 'O'", which could trivially be used to pare down word list. Instead,filter_targets
uses expensivecompare
calls determine which words to keep. I can't tell from the question description if the function names and arguments are fixed by a programming challenge type architecture, or if the program can be restructured with different function names, arguments, or objects. \$\endgroup\$