Goals of this assignment
This assignment is due Monday, November 29th at 11:59pm.
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
Collaboration Policy
You may collaborate with one other student on this assignment. You may write code with this one other student, and you may view each other’s code. You cannot write or read code with other students. If you collaborate with a partner, you must also document your work using commits to a Git repo.
Include in your README.txt
file the partner you worked with, if any. You should also include a link to the private GitHub repo that you’re using to host your code. We will be able to determine who was responsible for what code by reading the commits from your Git repo. When you submit, we also ask that you provide a screenshot of your mutual commit history from the repo.
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 Eclipse. From Preferences
, choose Java -> Editor -> Save Actions
and check the Format source code
box under the Perform the selected actions on save
option.
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
private
with 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.
When you submit, you should include in your README.txt
an overall analysis of the cohesion and coupling in your assignment. Most of your score here will come from accurately assessing your own work, rather than achieving a perfect design strategy. It will be important that you can describe places where it might have made more sense to make different decisions (splitting classes, relocating methods). You can also describe what challenges prevented you from making those design choices, if relevant.
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
We are not providing a required class layout for this assignment. The starter files give you a complete implementation of Hangman without the “Evil” twist. You can consider the starter files as a set of hints about how your implementation might be built, but there’s no requirement that you match anything in particular or use any part of it. You are welcome to use as much or as little of it as you like.
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. You’re not required to format your output in any specific way, but 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
Regardless of your design, we expect you to write unit tests for every public method. The only public methods that you can leave untested are those that perform file I/O or user I/O. You should keep the file I/O in as few methods as possible. If your program is effectively untestable because of widely distributed I/O operations, you’ll lose points for both insufficient testing and poor design choices.
There is no minimum coverage required to earn full points in testing. Instead, use a black-box testing approach to generate a number of interesting inputs for each of the methods you write. If you’re concerned that you might be missing interesting cases, then you can consider using coverage analysis to identify gaps in your testing strategy.
In order to perform tests, you should provide a way of setting up an Evil Hangman game with a smaller dictionary than the default one provided. Reading in the entire text file in order to unit test is going to be inefficient. You can use the five word example we’ve outlined above, as well as your own potential sets of words. Study the provided constructors in Hangman.java
to get a sense of how you might do this.
Submission
Submit all of the files that are necessary to run your Evil Hangman program. Submit all of your tests as well. Additionally, you should include a README.txt
that includes:
- who you worked with (if anyone)
- a link to your GitHub repo
- an assessment of the cohesion and coupling in your solution
- a description of how to run your program.
Finally, submit a screenshot of your GitHub repo if you worked with a partner.
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. By default, they will be found in a folder called eclipse_workspace
, often in your user’s directory. 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 Piazza or in Office Hours.