Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Python Python Collections (2016, retired 2019) Sets Out Of This Word Game

Out Of This Word - check for valid words. Is this the best way?

Hi,

At the end of the video Craig suggests what improvements could be made, so I added a function that checks whether the guess is actually contained in the challenge word.

The function is called 'check_valid_word' and it's included in the code below. I think it works ok, but my question is, is there a sipker way of doing this?

Thanks, Chris

import random
import os

WORDS = (
    "treehouse",
    "python",
    "learner")


def prompt_for_words(challenge):
    guesses = set()

    print("What words can you find in the word '{}'".format(challenge))
    print("(Enter Q to Quit)")

    while True:
        guess = input("{} words >. ".format(len(guesses)))

        # Don't allow words of less than 2 chars
        if len(guess) < 2:
            if guess.upper() == "Q":
                break
            else:
                print ("Guesses must be at least two letters - try again.")
                continue

        if check_valid_word(guess):
            guesses.add(guess.lower())
        else:
            print("Sorry, '{}' is not in '{}' - try agian.".
                  format(guess, challenge))
            continue
    return guesses


def check_valid_word(word):
        """Check whether the chars exist in the word."""
        guess_chars = []
        for char in word:
            guess_chars.append(char.lower())

        # 1. make a copy of the challenge_chars list
        check_list = challenge_chars.copy()
        # 2. Loop through the copied list and remove any matches
        for letter in guess_chars:
            if letter not in check_list:
                return False
            else:
                check_list.remove(letter)
        return True


def output_results(results):
    for word in results:
        print("* " + word)


def clear_screen():
    os.system("cls" if os.name == 'nt' else "clear")


# The Game
clear_screen()
challenge_chars = []
challenge_word = random.choice(WORDS)
# Create a list of chars in the challenge word
for char in challenge_word:
    challenge_chars.append(char)

player1_name = input("\nPlayer 1, what is your name? ")
player1_words = prompt_for_words(challenge_word)
player2_name = input("\nPlayer 2, what is your name? ")
player2_words = prompt_for_words(challenge_word)


# The results
clear_screen()
print("Results for " + player1_name + ":")
player1_unique = player1_words - player2_words
print("{} guesses, {} unique".format(len(player1_words), len(player1_unique)))
output_results(player1_unique)
print("-" * 10 + "\n")
print("Results for " + player2_name + ":")
player2_unique = player2_words - player1_words
print("{} guesses, {} unique".format(len(player2_words), len(player2_unique)))
output_results(player2_unique)
print("-" * 10 + "\n")
print("Shared guesses:")
shared_words = player1_words & player2_words
output_results(shared_words)
print("-" * 10 + "\n")

5 Answers

Jeff Muday
MOD
Jeff Muday
Treehouse Moderator 28,716 Points

Definitely what Steven (Parker) says is correct if your rules are such that you can only use a similar number of character repeats. Note that this is pretty close to the version Chris presented originally, just slimmed down a little.

In this case, I'd want to use an iterator over the lower case version of your word, and list conversion and remove() method from a guess_list. We loop over the word letter by letter and if that letter is in the guess_list, we remove it. If all the letters in the guess_list are consumed it is VALID. If any characters are left, then it is INVALID.

We could make this really short using a list comprehension and lambda functions, but in my opinion, it would be trading semantic simplicity/readability for looking slick!

def check_valid_word(word, guess):
    guess_list = list(guess.lower())
    for letter in list(word.lower()):
        if letter in guess_list:
            guess_list.remove(letter)
    if len(guess_list):
         return False
    return True

Now, let's see if it works in a console...

Python 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def check_valid_word(word, guess):
...     guess_list = list(guess.lower())
...     for letter in list(word.lower()):
...         if letter in guess_list:
...             guess_list.remove(letter)
...     if len(guess_list):
...          return False
...     return True
...
>>> word='Treehouse'
>>> guess='tree'
>>> check_valid_word(word, guess)
True
>>> guess='teehee'
>>> check_valid_word(word, guess)
False
>>> guess='ruby'
>>> check_valid_word(word, guess)
False
Steven Parker
Steven Parker
229,644 Points

Because this was a chapter on sets, my first inclination was similar to Jeff's — convert to sets and use set operations. But as sets only store one of each item, they aren't useful for keeping track of quantities. With set math, the check routine will think "teeheehee" can be formed from "treehouse", which it clearly can not. So your method is better suited to the game as it is.

But it would still be better if you could make sure the entries are valid words as well as being possible to construct. I'd probably approach that by abandoning the letter checking and keeping a list of possible answers to go with each challenge word. Not only could you assure validity of the words, when the user finishes you could tell them exactly how many of the possible words they missed.

Jeff Muday
MOD
Jeff Muday
Treehouse Moderator 28,716 Points

Nice work there! I like the clear comments.

There are always so many different ways of getting to a solution. You might consider using sets. Sets are unordered, and its elements must be unique so might appear a little strange at first, but are pretty useful.

If I convert the word "treehouse" to a set, it will become

['e','h','o','s','r','u','t']

though its order is never guaranteed.

But, we can check various set operations without ever having to use a loop.

suppose I guess "tree" and convert that to a set.

['e','r','t']

We can easily check if the set difference is empty or non-empty. See transcript below.

Python 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> word = 'treehouse'
>>> guess1 = 'tree'

>>> set(guess1)
set(['r', 'e', 't'])
>>> set(word)
set(['e', 'h', 'o', 's', 'r', 'u', 't'])

# since guess1  'tree' is entirely contained in word, if we look at set difference, we get the empty set
>>> set(guess1)-set(word)
set([])

# now... guess the word 'ruby' which is NOT entirely contained in 'treehouse'
# notice that the set difference is not empty!
>>> guess2 = 'ruby'
>>> set(guess2)-set(word)
set(['y', 'b'])
>>>

We can use this idea to write a function-- if we get an empty set, then it was a VALID word, if the set difference is not an empty set, it was NOT a valid word.

def check_valid_word(word, guess):
        """Check whether the guess chars exist in the word. Note, case insensitive converstion to lower"""
        if set(guess.lower()) - set(word.lower()):
            return False
        return True

Thanks both.

Similar to Steven I began using sets but realised as a set can not contain duplicate values it would not work in this case.

@Jeff your second suggestion appears to be similar to my approach although the way you create the initial list is a lot cleaner - so thank you for that!

guess_list = list(guess.lower())

vs

guess_chars = []
        for char in word:
            guess_chars.append(char.lower())

@Steven your suggestion of keeping a list of possible answer for each challenge word would surely be a huge task if there were say 100 words in the original list? My thinking was to check that the guess could be derived from the challenge word and then check that answer against some kind of external dictionary to ensure that the answer given was a genuine word.

Steven Parker
Steven Parker
229,644 Points

You could indeed do dictionary checks if you have an appropriate word data file handy. But that wouldn't give you the same scoring potential as a stored solution set.

I have played a game called Text Twist which I'm sure is implemented this way, because from the beginning of the game there are blank fields on the screen for every possible word. Guessing one just fills in one of the blanks. And they do have a very large (I'm not sure how many) database of words and pre-calculated solution sets.

I know that the focus of this discussion is the checking whether the guess word can be derived from the challenge word, but I do want to point out a different detail in OP's code: the length check for words longer than 2. I would remove that if statement because it can (1) lead to bugs and (2) be redundant.

(1) Some words are less than two letters long. For example, "a" and "I" are valid words. While the letter "i" isn't in any of the provided challenge words, "a" is in one of them: "learner".

(2) I know that there are a lot more possible single-letter guesses that are NOT "real" words. However, invalidity stemming from not being a "real" word is a kind of invalidity that the derivation/letter test also cannot catch, meaning that an additional check for "realness" must be performed anyway if we want to make sure the guess is a "real" word.

Steven Parker
Steven Parker
229,644 Points

FYI: That "Text Twist" game I mentioned only allows 3-letter or more words.

And I just discovered that it's available online from MSN Games if you'd like to see the layout for yourself — Text Twist.