DUE: 11:59 pm on Wednesday, November 26
Goals of This Assignment
In this assignment, you’ll modify and extend a Hangman program to create “Evil Hangman,” a game of Hangman in which the computer secretly changes the hidden word in order to make the player’s task as hard as possible.
Tools for Completing This Assignment
- File I/O
- Loops & Iteration
- Getting User Input from a Scanner
- Reading and Understanding Provided Code
- Working with Data Structures
- Unit Testing
Git
You should keep track of your code for this assignment using git. Review the instructions for setting up a git repository here. There are not specific checkpoints at which you should make a commit. Instead, you should commit and push your changes each time you make substantial progress.
You will submit as part of your solution a screenshot of your commit history. The image should show at minimum 5 commits to receive the full 5 points. You are welcome to make more.
Collaboration Policy
You cannot collaborate with anyone on this assignment. All code you write must be your own, and you may not discuss the assignment with other students. Please refer to the Collaboration & Academic Misconduct portion of the syllabus for further details.
Style & Design Policy
Good style includes:
- Consistent indentation in code, with lines of code indented to the correct level representing the block they belong to.
- Identifiers for methods and variables that follow the correct camel case convention and are appropriately descriptive.
- Correct spacing between operators, parentheses, and brackets.
- Closing File Streams after using them
Fortunately, most good style practices can be automatically enforced using IntelliJ.
Additionally, you’ll need to make good design choices for this assignment. We won’t be too strict, but some things to look out for are:
- Declaring variables only in the scopes where they’re needed and avoiding the creation of unnecessary fields
- Fields should be set to
privatewith access moderated by getter and setter methods - Classes should have clear responsibilities and no one class should represent multiple entities of a system.
- Taking steps to maximize cohesion and minimize coupling.
Style and design scoring will consist of 10% of the assignment grade.
Specifications
Access the starter files here.
Background
Hangman
The original, non-evil version of Hangman is a game wherein one player attempts to guess a hidden word letter by letter. When the player guesses a letter that’s contained inside of the hidden word, all locations where that letter appears in the hidden word are revealed. Any letters that have not been guessed in the hidden word should be represented by some special “unknown” character. There is a full description of the game (as well as some colorful history) from Wikipedia here.
We have provided for you an implementation of Hangman that chooses a random word from a dictionary (same as HW5’s dictionary). The user is prompted to guess a new letter until they have successfully guessed all of the letters contained in the game. Notice that this implementation does not have a limit to the number of guesses, and the user is not penalized for incorrect/repeated guesses.
You should run HangmanRunner.java and play the game a couple of times to get a sense of how it works. Observe the program’s output: unguessed letters are marked with underscores, guessed letters are represented exactly, and there is a sorted list of all letters that have been incorrectly guessed presented each time the user is asked to make their next guess.
Evil Hangman
In Hangman, the word that the user is trying to guess is fixed before they start playing. In Evil Hangman, the program maintains a set of words that match the pattern of correctly provided letters so far, but does not commit to any particular word in advance. If a user guesses a letter, the program will choose to assign that letter to the location(s) in the partial solution that leaves the most potential matches remaining. It might be the case that leaving the letter out keeps the most options open; in that case, you’d report the letter as being an incorrect guess.
Requirements for Submission
Class Design
The starter files give you a complete implementation of Hangman without the “Evil” twist. To create the “Evil” version of the game, you will create “Evil” versions of the provided files. Specifically, your solution should have the following files:
EvilHangmanRunner.javaEvilHangman.javaEvilSolution.java
Make sure you understand how the starter files are connected and funtion first! The “Evil” versions may reuse or slightly modify code from the starter files as well as add new functionality. Detailed requirements for the new “Evil” files are found below.
EvilHangmanRunner and EvilHangman
These two classes should be virtually identical to the provided HangmanRunner and Hangman classes, respectively. Only minor updates are necessary. Specifically, look for what changes need to be made in terms of…
- What data types are being used
- How data types are constructed
EvilSolution
The following public methods are required to be implemented in EvilSolution:
EvilSolution(ArrayList<String> wordList)boolean isSolved()void printProgress()boolean addGuess(char guess)String getTarget()
The implementation details for each of these methods may be different than the provided starter code. It is up to you to determine what needs to change and how. The same applies for determining what fields to create. Note the constructor takes an initial list of words as opposed to a single String. You are allowed to write as many new private helper methods as you need to accomplish these tasks.
Inputs & Outputs
You can assume that all words in the provided dictionary will be in lowercase, and that user inputs will only contain lowercase letters. Your submission’s default behavior should be to recommend words from engDictionary.txt, provided to you here and in HW5.
We recommend that you keep a clean version of our original Hangman project so that you can verify that your game loop matches ours exactly. For each turn that the player takes, you should:
- Ask them to enter a letter
- Print the current progress, including…
- the partial solution so far
- the sorted, incorrect letters that they’ve already guessed.
- If the user enters something that’s not just a single letter, politely ask the user to try again.
- If the user enters a letter they’ve already guessed, politely ask the user to try again.
- If the user enters a new letter, place it in the locations that provide the largest collection of possible future moves.
Once the user has guessed all letters in the word, you should print out a “game over” message, indicating what the hidden word was.
Turning Hangman into Evil Hangman
Instead of fixing a single hidden word at the start of the program, Evil Hangman will choose a random length and maintain a collection of all words in our input dictionary with that particular length. Whenever the player guesses, the program should partition the words into “word families” based on the positions of the guessed letters in the words. For example, if our full word collection is <"echo", "heal", "belt", "peel", "hazy">, the player might start by guessing the letter 'e'. This would generate four different consistent word families:
| Partial Solution | Word Family |
|---|---|
e--- | <"echo"> |
-e-- | <"heal", "belt"> |
-ee- | <"peel"> |
---- | <"hazy"> |
Once we’ve calculated our word families, we should choose the largest remaining word family to proceed with. In that case, we would update our partial solution to be -e-- and our remaining collection of words would be <"heal", "belt">. Note then that a blank does not always mean that it can contain any character. Instead, a blank indicates that any character that has not already been guessed might be placed there.
The Evil Hangman program is not permitted to reverse its decisions once it has determined which letters to reveal. If we continue our previous example, the user might next guess the letter 'l'. Now, the program must generate word families based on all possible positions of 'l' into our partial solution that already consists of -e--. The program could no longer consider, for example, the words "peel" or "hazy", since neither of them follow the pattern of the partial solution we’ve already committed to.
Implementing Word Families
We provide two ideas of implementations for collecting word families. Both have their virtues and drawbacks. You are welcome to use either implementation, or another of your own design. You might, for example, think about whether a HashSet might be useful in this case.
ArrayList of ArrayLists
The set of all possible word families can be expressed as an ArrayList of the word families. Each word family itself can be represented as an ArrayList of Strings in that family.
In this case, the word families for handling 'e' in the previous example might look like the following:
[["echo"], ["heal", "belt"], ["peel"], ["hazy"]]
The best word family to pick would therefore be the longest ArrayList in your ArrayList.
HashMap of ArrayLists
We can still choose to represent each individual word family as an ArrayList of Strings in that family. We might store these ArrayLists as the values of a HashMap. The keys in this case might be the partial solutions that define their own word families.
As before, here is how you might think about this implementation in terms of the 'e' placement example from before.
"e---" --> ["echo"]
"-e--" --> ["heal", "belt"]
"-ee-" --> ["peel"]
"----" --> ["hazy"]
Hidden Evil
Your goal should be to ensure that the player never notices that the underlying solution is changing. Do not print anything different from our standard game of Hangman that might indicate to the player that the game is rigged.
Testing
We expect you to write unit tests for every public method in EvilSolution.java in a file called EvilSolutionTest.java. We will run your tests in addition to our own to verify your code works. Additionally, the code coverage of these tests should be at least 80% in order to get full coverage points.
Reading in the entire text file in order to run unit tests is going to be inefficient. Instead, pay attention to how an EvilSolution object is constructed and write your tests to take advantage of this set up. You may still want to create additional, smaller text files for testing. Any text files created for unit testing must be submitted alongside your code in order for us to run your tests.
Additionally, you must write at least three tests for the start method in EvilHangman.java in a file called EvilHangmanTest.java. Even though most of the code in this class is provided, we want you to practice writing tests for methods which handle user I/O. One way to approach these tests is to first play the game and think about interesting user input combinations. For example, what are potential errors users could make when entering input?
Submission
Submit all of the files that are necessary to run your Evil Hangman program (EvilHangmanRunner, EvilHangman, and EvilSolution). Submit all of your tests as well (EvilSolutionTest, EvilHangmanTest, and any additional dictionary files necessary to run your tests). Finally, submit a screenshot of your git log history.
Ensure that your files do not have a package header! Specifically, the first lines of your file should not read package xyz or similar.
Please find these files locally on your file system. You should select each of the files and upload them as files (not as a directory, and preferably not as a .zip either). If you need help, please reach out on Ed or in Office Hours.