handout #16

CSE143—Computer Programming II

Programming Assignment #4 (bonus)

due: Thursday, 2/15/18, 9 pm

This is an extra credit assignment that is worth just two points.  It is intended for students who want to explore how to improve the HangmanManager and how to use inheritance to create this variation.  There will be minimal assistance available to students working on this problem.

In this version, you will improve on two flaws in the original HangmanManager:

·         We will modify the record method so that it wins immediately if it can when the user is down to one guess (making the program even more evil)

·         We will modify the words() and guesses() methods so that they prevent a client from changing our internal data structures

Each of these will be worth 1 point.  You can attempt just one bonus or you can attempt both.

Both of these changes will be implemented through inheritance.  The idea is that we want to create a minor variation of the original class and inheritance is particularly good at capturing that kind of variation.  We can override particular methods and change their behavior.  Remember that you can still call the superclass version of a method even if you are overriding it.  If the superclass has a method foo, you can call super.foo to call the overridden method.

You should define a class called HangmanManager2 that extends HangmanManager.  You will have to include a constructor for HangmanManager2 that takes the same parameters as HangmanManager.  It will need to have a call on the superclass constructor as the first line of the new constructor.  You call a superclass constructor by saying:

super(param, param, …, param);

The textbook has an example of calling the superclass constructor on pages 600 to 602 (chapter 9).

You should modify the following line of code in HangmanMain to have it use the new version of HangmanManager:

HangmanManager hangman = new HangmanManager2(dictionary2, length, max);

Notice that the only thing we have to change is the kind of HangmanManager being constructed (in this case, a HangmanManager2).

In terms of grading, if you get one of the two modifications to work correctly, you will get one bonus point.  To get the second bonus point, you have to have both working and you have to use good programming style.  For example, you wouldn’t want to include methods in your class that don’t need to be there.  If you are not changing an inherited method, then it doesn’t need to appear in the subclass.  In terms of comments, you don’t have to repeat the comments from the superclass.  You should instead assume that someone is familiar with the superclass description of the methods and should describe how the behavior is changed.

Your solution should work for any legal implementation of HangmanManager.  This means that you can't solve the problem by modifying your solution to HangmanManager (e.g., adding new methods or making fields protected).

You should name your file HangmanManager2.java and you should turn it in electronically from the “Homework” tab on the class web page if you choose to do this bonus.  The details for each bonus point are included below.

Even More Evil (1 point)

There are many improvements you can make to the evil hangman algorithm to make it even more evil.  For this bonus point, you will implement one specific improvement.  Think of what happens when the user has just one guess left.  We go through our normal process of splitting the current set of words into buckets and then picking the bucket with the most words.  By doing so, we are likely to keep the user alive to try to win the game.  It would be better to just end it if we can by having the player lose.

Consider, for example, the following log using dictionary.txt:

Welcome to the cse143 hangman game.

 

What length word do you want to use? 5

How many wrong answers allowed? 5

 

guesses : 5

guessed : []

current : - - - - -

Your guess? a

Sorry, there are no a's

 

guesses : 4

guessed : [a]

current : - - - - -

Your guess? e

Sorry, there are no e's

 

guesses : 3

guessed : [a, e]

current : - - - - -

Your guess? i

Sorry, there are no i's

 

guesses : 2

guessed : [a, e, i]

current : - - - - -

Your guess? o

Sorry, there are no o's

 

guesses : 1

guessed : [a, e, i, o]

current : - - - - -

Your guess? u

Yes, there is one u

 

guesses : 1

guessed : [a, e, i, o, u]

current : - u - - -

Your guess? ...

Why let the user keep playing when there is only one guess left?  Instead, we will choose the first word in the current set of words that doesn’t contain the letter the user has guessed.  For the log above, instead of saying that there is one “u”, the program instead says:

guesses : 1

guessed : [a, e, i, o]

current : - - - - -

Your guess? u

Sorry, there are no u's

 

answer = byrls

Sorry, you lose

We will accomplish this by overriding the record method.  It will still call the superclass version of the method to do most of the work.  But before doing so, it will check to see if the user has just one guess left.  If so, then it will go through the current set of words and pick the first word that does not contain the letter being guessed.  If such a word can be found, then it calls the clear method on the current list of words and then adds this word back into the current set of words.  That way when the superclass version of record is called, it will find just one word to work with.  That one word will cause the user to lose the game immediately.  If the number of guesses left is not 1 or if no such word can be found, then it simply calls the superclass version of the record method so that the behavior is unchanged.

Protecting Internal Structures (1 point)

The obvious way to write the words() and guesses() methods is to return a reference to fields of your HangmanManager object and we asked you to solve it that way in writing HangmanManager.  This is very dangerous, because it means that clients have the ability to damage the internal state of your objects.  We could make copies of them to return, but copying is expensive.  A better approach is to return an unmodifiable version of each that allows the client to perform “read only” operations but prevents the client from performing any mutating operations.

The Collections class in java.util has a series of methods that can be used to construct unmodifiable versions of various collections.  For example, if you look at HangmanMain, you will see that it includes this line of code:

List<String> dictionary2 = Collections.unmodifiableList(dictionary);

It passes the modifiable dictionary to the method Collections.unmodifiableList which returns a reference to a new list that is a “wrapper” for the original.  It doesn’t make a copy, but it prevents a client from making any changes to the new version of the structure.  There are similar methods called Collections.unmodifiableSet and Collections.unmodifiableSortedSet that can be used to create unmodifiable versions of the words and guesses sets.

For this bonus point, you will override the words() and guesses() methods to return unmodifiable versions of the sets returned by calls on the superclass versions of these methods.

One simple way to do this would be to override the words() and guesses() methods by always constructing a new unmodifiable object.  That would be inefficient because it constructs a brand new object every time.  At the other extreme, you could construct unmodifiable versions once in your constructor and then never change them.  That won’t work, though, because the set objects might change.  The algorithm we are using for evil hangman involves replacing our old set of words with a new set of words and sometimes that will involve having a new set object.  So whatever approach we use has to take into account the fact that sometimes we have a new set object to work with but not always.

To get this bonus point, you have to implement this in the efficient way.  In particular, your class should create new unmodifiable versions of each set if and only if there is a new object being returned for the original set.  For example, suppose that on 10 calls to the words() method, there are four different actual set objects returned.  Then your class should create four different unmodifiable sets.  If you create fewer than four, then your class won’t work.  If you create more than four, then you won’t receive the credit.

Keep in mind that what matters is whether the set is a brand new object, not whether it has a new state.  If I add or remove values from a set but I’m still using the same set, then that is a single object.  But sometimes the change involves the object itself (a different set object).  So your class has to figure out when the underlying set object has changed.  You might guess that this happens whenever the record method is called, but you aren’t allowed to make assumptions like that.  For example, the superclass might never change the set of words.  You also might think that a change in the size of the set would signal that it is a new set but once again, this is an assumption you aren’t allowed to make.  It is possible that the same set object will have different sizes at different times.

You should solve this problem by comparing object references.  Recall that you can compare object references for equality.  For example, consider the following code:

Set<String> s1 = new TreeSet<String>();

Set<String> s2 = new TreeSet<String>();

Set<String> s3 = s1;

This constructs two set objects but introduces three set variables.  The variables s1 and s3 refer to the same object while the variables s1 and s2 refer to different objects.  You can test that with code like the following:

if (s1 == s2) {

    System.out.println ("yes");

} else {

    System.out.println("no");

}

 

if (s1 == s3) {

    System.out.println("yes");

} else {

    System.out.println("no");

}

This prints “no” followed by “yes.”  The variables s1 and s2 aren’t equal because they refer to different objects.  But the variables s1 and s3 are equal because they refer to the same object.  You should use tests like these to implement the second bonus point to detect when you have a new set.