Introduction

In this project, you will create a retro text version of the Wheel of Fortune, a word guessing game.

The project combines functional and object-oriented programming paradigms, focusing on the material from Chapter 2.5 of Composing Programs. The project also involves understanding, extending, and testing a large program.

The wheel.zip archive contains several files. Your changes will be made to:

  • board.py
  • game.py
  • player.py
  • secret.py
  • wordset.py

You can obtain all the files needed for this project by downloading this zip archive.

Logistics

This is a 20-day project. You may work with one other partner. You should not share your code with students who are not your partner or copy from anyone else's solutions.

In the end, you will submit one project for both partners. The project is worth 20 points. 18 points are assigned for correctness, and 2 points for the overall composition of your program.

You will turn in the following files:

  • answers.txt
  • board.py
  • game.py
  • player.py
  • secret.py
  • wordset.py
  • human_session1.txt
  • computer_session1.txt
  • computer_session2.txt

You can submit by uploading these files to OK.

You will be able to view your submissions on the OK dashboard.

For the functions that we ask you to complete, there may be some initial code that we provide. If you would rather not use that code, feel free to delete it and start from scratch. You may also add new function definitions as you see fit.

However, please do not modify any other functions. Doing so may result in your code failing our autograder tests. Also, please do not change any function signatures (names, argument order, or number of arguments).

None

Testing

Throughout this project, you should be testing the correctness of your code. It is good practice to test often, so that it is easy to isolate any problems.

We have provided an autograder called ok to help you with testing your code and tracking your progress.

The primary purpose of ok is to test your implementations, but there is a catch. At first, the test cases are locked. To unlock tests, run the following command from your terminal:

python ok -u --local

This command will start an interactive prompt that looks like:

=====================================================================
Assignment: Wheel of Fortune
OK, version ...
=====================================================================

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unlocking tests

At each "? ", type what you would expect the output to be.
Type exit() to quit

---------------------------------------------------------------------
Question 0 > Suite 1 > Case 1
(cases remaining: 1)

>>> Code here
?

At the ?, you can type what you expect the output to be. If you are correct, then this test case will be available the next time you run the autograder.

The idea is to understand conceptually what your program should do first, before you start writing any code.

Once you have unlocked some tests and written some code, you can check the correctness of your program using the tests that you have unlocked:

python ok --local

Most of the time, you will want to focus on a particular question. Use the -q option as directed in the problems below.

The tests folder is used to store autograder tests, so make sure not to modify it. You may lose all your unlocking progress if you do. If you need to get a fresh copy, you can download the zip archive and copy it over, but you will need to start unlocking from scratch.

Core Concepts

This project will give you a chance to work with classes as a means of composing large software out of well-designed components. You get to build a computerized version of the popular American game show Wheel of Fortune using object oriented programming. Although the whole project is not very large, we want to give you the feeling of working with classes at the larger scale. The game is built from a collection of classes partitioned into several files. The idea is that you should not be looking inside a class when writing code that uses it. You should only pay attention to the interface, i.e., the attributes and methods defined on objects of the class and, to a lesser degree, the attributes and methods on the class itself.

The game

The following shows a little example of playing the game where the computer picks the word and the human guesses.

['_', '_', 'a', 't']
Guessed chars:  ['e', 'a', 't', 'f']
guesser , please enter your next guess.
h

['-', 'h', 'a', 't']
Guessed chars:  ['e', 'a', 't', 'f', 'h']
guesser , please enter your next guess.
t
Please enter a single character not yet guessed
w
(['w', 'h', 'a', 't'], 6)
>>>

Of course, why should it be played that way... You might want to pick the word and have the computer do the guessing. Ahh, that would require some data8 thinking. What letter would be most likely to appear in the word, given all that you know so far?

>>> inhuman()
picker , pick your secret word.
consecrate
['_', '_', '_', '_', '_', '_', '_', '_', '_', '_'] []

['_', '_', '_', '_', '_', '_', '_', '_', '_', '_']
Guessed chars:  []
30824  possible words.
Computer guesses:  e
['_', '_', '_', '_', 'e', '_', '_', '_', '_', 'e'] ['e']

['_', '_', '_', '_', 'e', '_', '_', '_', '_', 'e']
Guessed chars:  ['e']
813  possible words.
Computer guesses:  r
['_', '_', '_', '_', 'e', '_', 'r', '_', '_', 'e'] ['e', 'r']

['_', '_', '_', '_', 'e', '_', 'r', '_', '_', 'e']
Guessed chars:  ['e', 'r']
25  possible words.
Computer guesses:  a
['_', '_', '_', '_', 'e', '_', 'r', 'a', '_', 'e'] ['e', 'r', 'a']

['_', '_', '_', '_', 'e', '_', 'r', 'a', '_', 'e']
Guessed chars:  ['e', 'r', 'a']
11  possible words.
Computer guesses:  t
['_', '_', '_', '_', 'e', '_', 'r', 'a', 't', 'e'] ['e', 'r', 'a', 't']

['_', '_', '_', '_', 'e', '_', 'r', 'a', 't', 'e']
Guessed chars:  ['e', 'r', 'a', 't']
6  possible words.
['consecrate', 'palpebrate', 'perpetrate', 'subserrate', 'uniserrate', 'vertebrate']
Computer guesses:  p
['_', '_', '_', '_', 'e', '_', 'r', 'a', 't', 'e'] ['e', 'r', 'a', 't', 'p']

-----

['_', '_', '_', '_', 'e', '_', 'r', 'a', 't', 'e']
Guessed chars:  ['e', 'r', 'a', 't', 'p']
6  possible words.
['consecrate', 'palpebrate', 'perpetrate', 'subserrate', 'uniserrate', 'vertebrate']
Computer guesses:  s
['_', '_', '_', 's', 'e', '_', 'r', 'a', 't', 'e'] ['e', 'r', 'a', 't', 'p', 's']

-----

['_', '_', '_', 's', 'e', '_', 'r', 'a', 't', 'e']
Guessed chars:  ['e', 'r', 'a', 't', 'p', 's']
3  possible words.
['consecrate', 'subserrate', 'uniserrate']
Computer guesses:  c
['c', '_', '_', 's', 'e', 'c', 'r', 'a', 't', 'e'] ['e', 'r', 'a', 't', 'p', 's', 'c']

-----

['c', '_', '_', 's', 'e', 'c', 'r', 'a', 't', 'e']
Guessed chars:  ['e', 'r', 'a', 't', 'p', 's', 'c']
1  possible words.
['consecrate']
Computer guesses:  n
['c', '_', 'n', 's', 'e', 'c', 'r', 'a', 't', 'e'] ['e', 'r', 'a', 't', 'p', 's', 'c', 'n']

-----

['c', '_', 'n', 's', 'e', 'c', 'r', 'a', 't', 'e']
Guessed chars:  ['e', 'r', 'a', 't', 'p', 's', 'c', 'n']
1  possible words.
['consecrate']
Computer guesses:  o
(['c', 'o', 'n', 's', 'e', 'c', 'r', 'a', 't', 'e'], 9)

And why should we settle for just a two-player game. Let's get all wheel of fortune and allow multiple guessers.

The Code

Most concepts in the game have a corresponding class that encapsulates the logic for that concept. To make this project concrete, we have gone through a process similar to what we did together in lab to design a set of classes for this game. In the project you will implement these classes, but hopefully in the future, when you are designing software systems from scratch, this will help you think about that design process.

The classes go together to make a game flow such as the following 2-player game with a computer picking the word and the human guessing the letters:

from wordset import Dictionary
from player import Player, ComputerPlayer, HumanPlayer
from game import Game

def human_plays():
    dictionary = Dictionary("assets/lincoln.txt")
    Player(dictionary)
    picker = ComputerPlayer()
    guesser = HumanPlayer("guesser")
    game = Game(picker, [guesser] )
    board =  game.play(True)
    print("Solved ", board.word()," in ",len(board.guesses()), "guesses")

if __name__ == "__main__":
    human_plays()

The Dictionary class takes a path for a file, reads in the file and extracts all the words in it to build a set of words to be used for the game. (Here we are using Lincoln's Gettysburg address.)

Player is a base player class defines the interface used by all type of players. In constructing it, a common dictionary is made available to all the player objects that may be involved in the game. Two player classes are derived from Player; they are HumanPlayer and ComputerPlayer. We will look at these in more detail below. One player picks the word. The other player (or players) take turns guessing. Here, like a conventional computer game, the ComputerPlayer object picks the word and the HumanPlayer object guesses - by allowing you to type to it.
We build a Game with these players. We then let the Game.play method carryout the whole process - ask the picker player for the secret word, hide that, create a Board, cycle through the guessers letting them guess until they miss.
When the game is all done, the board object is returned, conveying the entire history of play.

These classes are defined in several small files. We briefly describe the classes here. You will implement most of them, testing as you go.

To start a human player version of text-based game, run

python human.py

Secret Words

The SecretWord class represents the secret word that the picker has in mind and the guesser(s) need to guess. It is implemented in secret.py. It has four special methods and match. Collectively, these provide the abstraction of hiding a secret word. An object is constructed with the word hidden inside. (You'll need to use an attribute for that.) The __repr__ and __str__ special methods are written for you (to suggest that the word is secret). The object can be asked for the length of the secret word (len of the object) and match can be given a character which it matches against the secret word and returns a list of indices of characters that match. Notice that the doctest is on the class. It shows how the methods work together. It does nor reveal the object attributes that might be used, nor the representation.

Word Sets

The WordSet class represents what the players keep in their heads - a bunch of words. It is implemented in the file wordset.py. A WordSet is constructed by providing it a text string or a list of words. In the first case, it splits the string into words. In either case, it strips out the punctuation (remember string.punctuation) and converts the word to lowercase. (A function is provided in utils.py to help with that.) Each word should appear only once in the WordSet, i.e., eliminate the duplicates.
The words method returns a sorted list of the words in the WordSet. This does not mean that they are represented internally as a sorted list, but that is certainly one option. The __contains__ special method tests whether a word is in the WordSet. This allows the in operator to be used in expressions with a WordSet as shown in the doctest. You are to implement the three methods. You can use whatever data structure you like to store the words, but you might want to look at set.

Dictionary

The Dictionary class represents the set of words that is being used in the game. Only words in the dictionary can be picked. It is also in wordset.py. It inherits from WordSet but redefines the __init__ method to obtain the words from a file. We have implemented this for you in order to show the preferred method of opening and reading a tile.

#
# Dictionary class
#
class Dictionary(WordSet):
    """Construct a dictionary from all the words in a text file.
    Subclass of WordSet with a file based initializer.

    >>> from wordset import Dictionary
    >>> Dictionary('assets/lincoln.txt').words()[55]
    'government'
    """
    def __init__(self, filename):
        with open(filename) as fp:
            text = fp.read()
            WordSet.__init__(self, text)

Notice also how it uses the superclass to invoke the initialization method in that class.

Board

The Board class represents the board that holds the state of the game. Logically, it holds the sequence of spaces that represent the characters to be guess in the secret word. It also keeps track of all the characters that have been guess so far. When it is created it is provided with a SecretWord object that encapsulates the secret word. It never peeks inside, but it holds on to it. The Board object provides a set of methods that allow players to get information about the board - the length of the word that is being guessed, the list of characters that have been guessed so far, with the unguessed positions represented by '_', the guesses, hits, and misses so far.

It also has methods that are used to play on the board. The guess method is the main operation that moves the game forward. When a player guesses a character, it is applied to the board using this method. The done method determines if the entire word has been guessed, i.e., the game is done.

We have provided you with the display method, which presents the state of the board to the screen for the human user. It reads the little stick figures from text files in assets.

Player

The Player class is a base class that provides only an interface. It is in player.py. This interface is used by all types of players. The init method receives a dictionary that is to be used by all players. It uses this to set the class attribute all_words.

HumanPlayer

The HumanPlayer class inherits from Player and implements that interface. It provides the interface to screen and keyboard to support the human player. We have provided this for you so that you do not need to worry about how to format the I/O.

class HumanPlayer(Player):
    """HumanPlayer is initialized with a name and implements the player interface
    such that:
    - guess requests a guess from a person, via the input device
    - pick_word requests a secret word and verifies that it is in the dictionary

    """
    def __init__(self, name):
        self.name = name

    def guess(self, board):
        """Guess a character."""

        print(self.name, ", please enter your next guess.")
        guess = input()
        while (len(guess) != 1) or (guess in board.guesses()):
            print('Please enter a single character not yet guessed')
            guess = input()
        return guess

    def pick_word(self):
        """Return a secret word from the dictionary."""

        print(self.name,", pick your secret word.")
        word = input()
        while not word in Player.all_words:
            print(word, " is not in the dictionary. Another:")
            word = input()
        return word

The pick_word method asks the human for a word and makes sure that it is in the dictionary. The guess method asks the human for a character and makes sure that it is a valid guess.

ComputerPlayer

The ComputerPlayer class is the fun part. It has the same methods as the HumanPlayer but it has to do inference in the characters that have been guessed so far, looking at the dictionary, in order to make a good guess. You get to have fun with how it might pick words for you to guess.

WordMunch

The WordMunch class is also in wordset.py. This is designed to help your ComputerPlayer. Think about how you are going to guess characters from looking at the board and looking at the dictionary. What are the tricks that you use when you guess? Generally you start with e because it is the most frequently used character in the English language.
WordMunch inherits from WordSet and is intended to hold smaller and smaller set of words, typically starting with the full dictionary, as the guesses narrow down the possible words that could be the secret word. The frequency method computes the number of times each lowercase character occurs in all the words contained in the WordSet. The filter method discards all the words that don't satisfy some predicate, as specified by the higher order function that is passed to it. It applies the function to every word, keeps the ones that come out Truth-y and discards the others, thereby narrowing down the possibilities.

Game

The Game class implements the taking turns among players. It is in game.py. The initialization receives the player objects - the picker and all the guessers. The play method carries out the logic of the game. It has the picker pick a word. Makes a secret out of it. Creates the initial board with this secret. It then cycles through the guessers, giving each a chance to guess repeatedly until it misses. The winner method returns the player that won the game after it is done.

Problems

Now that we have introduced you to the design of all the classes, it's time to get to work building the game.

Problem 0 (1 pts)

Answer the following questions with your partner after you have read the entire set *.py files. If you cannot answer these questions, read the file again or ask a question on Piazza.

  • In SecretWord, you want the secret word to be private. How do you name object attributes to indicate that it is private?
  • What data structures could be used to hold the word set? What would make one choice better than another?
  • Why is it important that WordSet.words return a sorted list of words in the word set, when all we use it for is to test if a word is in the WordSet?
  • How does Dictionary invoke the initializer for its superclass?
  • Do Players ever access the SecretWord object? What does Board reveal about the secret word?
  • How will object methods of subclasses of Player access the all_words class attribute?
  • Why does the Player base class define methods that don't do anything?

Put your answers in a text file titled answers.txt and submit with your project.

Problem 1 (3 pts)

Complete the implementation of the __init__, __len__, and match methods in the SecretWord class in file secret.py.

Test your implementation before moving on:

python ok -q SecretWord --local

Problem 2 (2 pt)

Complete the implementation of WordSet.

Test your implementation before moving on:

python ok -q WordSet --local

Now that you have a WordSet, verify that you also have a working Dictionary that inherits from this. You didn't have to build the Dictionary, but you need to make sure that the one we provided works with your WordSet.

Test our implementation before moving on:

python ok -q Dictionary --local

Problem 3 (3 pt)

Complete the implementation of Board. Please note that this has several small methods that work together. The doctest tests them in combination. It does not depend on the representation that you choose to use for the board and for keeping track of the letters than have been guessed, hits, or misses. You will need to decide on the representation. And you will likely want to produce tests for your individual methods.

Test your implementation before moving on:

python ok -q Board --local

Problem 4 (3 pt)

Complete the implementation of Player. This is just the initialization method to set the class attribute.

Test your implementation before moving on:

python ok -q Player --local

Complete the implementation of the pick_word method of ComputerPlayer. You don't need the (much bigger) guess method.

Test your implementation before moving on. This you will have to do by exercising your ComputerPlayer.pick_word. Since it needs to yield a random word, it is hard to test deterministically. It is very simple, but make sure it works. Hint: you will want to look at random.sample.

Complete the implementation of the Game class in game.py.
The init method is provided with a player object that is the picker and a list of players that are the guesses. The game engine is the play method. It gets the secret word from the picker, turns it into a SecretWord object and creates the board with this secret. Then it cycles through the guessers until the game is done. A guesser player is presented with the board by invoking its guess method. The resulting guessed character is the played onto the board with the board's guess method. If the guess is successful, the player gets to keep playing. When it missed, the next player gets a turn. Finally, the winner method can be invoked when the game is over to obtain the winning player.

Testing the game is a little trickier, because it normally works with player object that take human input or have non-deterministic behavior. We've provided dummy players that you can test your Game against these before you connect your HumanPlayer and picker-only version of ComputerPlayer.

Test your implementation before moving on:

python ok -q Game --local

At this point you should have a complete human player game. Run:

python human.py

Cut and paste an example session with computer picking a word and the human guessing letters. Submit this as human_session1.txt.

Have fun. When you get tired of Lincoln, go find a more interesting book to use for words. The bigger the book the better chance you'll have against the machine. You can grab Shakespeare from the earlier homework. Or on Unix machines, including macs, you can use /usr/share/dict/words. Have fun.

Problem 5 (4 pt)

Now you get to turn the tables and build the computer guesser. You will do a sequence of implementations of the guess method in ComputerPlayer. The first step is to use the dictionary in the board and WordMunch to get a set of words to work with in making guesses. As a start, guess the most frequent letter that has not been guessed. Try this out using.

python computer.py

You get to pick words and see what this algorithm has trouble with. Observe how many guesses it takes to guess words that you give it.

Next, filter out all the words in the dictionary that are not the correct length. You can use the frequencies of letters in the filtered list to make your guess.

Cut and paste an example session with computer guessing a word you pick. Submit this as computer_session1.txt.

Note: We will be providing an additional test for this case and the next well before you get to it.

Problem 6 (2 pt)

Improve your ComputerPlayer by using character frequency in the filtered subset of the dictionary consisting of words of the right length. At each successive guess, use the correctly guessed characters to filter out words of the right length that do not match these characters.

Cut and paste an example session with computer guessing a word you pick. Submit this as computer_session2.txt.

Once you have completed all questions, you can run all the OK tests at once using the command below.

python ok --local

Congratulations on finish the project!

Extra Credit (2 pts)

You can choose one of the options below to receive 2 additional points for the project. The tasks below are fairly open-ended, we want you to get creative! If you choose to complete the extra credit, please submit a text file called extra_credit.txt to the Wheel of Fortune (Extra Credit) assignment on OK. Please make sure you submit to the extra credit assignment (not the Wheel of Fortune assignment) in order to get credit. This text file should contain a description of the code you wrote, sample output from the game (if that helps explain the improvements), and a list of the files where the extra credit code is located. Make sure to add your partner to the extra credit assignment as well.

  • Implement a multiplayer version of computer player.
  • Instead of character frequencies for all unused characters in the possible words, look at character frequencies for the specific unguessed slots. Build a frequency per slot and use that in making the guess.
  • While it might seem wise to make the most likely guess, a rather different choice is the guess that will put you on the shortest path (fewest misses) to a unique determination. Once you know the word you should not miss at all. This approach introduces a bit of a search tree.
  • Come up with better ways of guessing, such as looking at character combinations. Show how much better these perform than simply filtering on length and match.