Flip in OOD

Part 1. Implementation

In this assignment, you will use Mark Cohen's game playing framework (paper | source code) to implement the Flip game. Play the game a few times—it is more subtle than it appears at first.

To practice playing with a (very stupid) opponent, try this application:

Program doesn't start? Install the latest version of Java: GetJava Download Button

H@x0r$—don't bother decompiling that program. It uses a different implementation strategy.

For your convenience, I give you a class AbstractBoard that implements the IBoard interface, providing a move stack as well as the collection of players.

You need to supply a subclass FlipBoard and one or more classes that implement the IMove interface. Supply a class Die that implements the IPiece interface. Obviously, a die should have a value and a method flip that flips the value. It should also have a flag that determines whether it is currently flippable.

The game state consists of

Note: Don't implement equals and hashCode for Die. You want their identity, not their face value, when you collect them in hash sets.

After each move, you simply swap the sets for each player. That little trick greatly simplifies the logic in the various methods that you need to implement.

For debugging, you should have a toString method that prints the game state.

As a warm-up, implement the flip move. It flips a flippable die and makes it unflippable. You need to be able to undo the move. (The minmax player will try out moves and undo them.) Undoing a flip is simple: flip the die and make it flippable again. Produce a suitable class implementing IMove and then do the right thing when an instance is passed to doMove/undoMove.

Remember to swap the die sets for the current and the other player at the end of doMove and the beginning of undoMove.

In the moveIterator method, build a collection of all possible flips. There is one for each flippable die of the current player. Then call iterator() on your collection.

Also, generate an instance of a flip in the parseMove method. (Use a string such as "f5" to indicate "Flip 5".) Make sure to check that the requested die is actually flippable.

Finally, implement the getWinner method. A player is a winner when the opponent has no dice left and has no opportunity for taking another die. That is, all the dice in the middle have value ≥ the die that was just played. (Call getTurn to find out who the current player is.)

Now you can run the game, playing a console player against a random player. (You need to edit the PlayGameConfig.xml file.) Of course, all you can do is flips, so the game play is going to be dull. Let's move on.

Making a "play" means moving an opponent's die to the middle and remembering it as "just played". Also, the current player's unflippable dice become flippable again. In your move class, you will need to remember those dice so that you can undo that operation. Make the necessary changes in doMove, undoMove, parseMove, and moveIterator. (Use a string such as "p5" to indicate "Play the opponent's 5".)

Now run the game again. The game is still dull because you can't take back dice when you have been "played".

Finally, add the part where you can take dice from the middle if you had just been "played". We want to carefully separate the decisions of each player. To achieve that, your opponent's turn ends immediately after "playing" you, and your first action is to take dice from the middle before doing a flip or your own "play".

That is, a move consists of

As always, your IMove objects need to have sufficient data for carrying out the undo operation. Since you will null out the "just played" die, you need to remember it for undoing.

Modify the parseMove method so that the taken dice are specified before the flip or play. For example, "112f1" means “take two 1s and a 2 from the middle, then flip a 1”. (It is legal to flip a die that you just took.) You must check that there is a sufficient quantity of dice in the middle. For example, the move given above is illegal if there is only a single 1 in the middle.

Finally, in the moveIterator, enumerate all subsets of the dice whose value is less than the die that has just been played. Use these to produce all legal moves. That's it—you are done! Enjoy a relaxing game of Flip against the MinMaxAlphaBetaPlayer.

Note: The MinMaxAlphaBetaPlayer isn't very good. To make it better, increase the cutoff and consider providing an evaluator. For faster searching, revisit your moveIterator to remove redundant moves. (For example, if there are two flippable dice of the same value, the iterator only needs to report one of them.) This part is entirely optional.

Part 2. Critique

Work with your buddy. Discuss the topics below with each other and record your consensus. Each of you does 50% of the writing.

Note: These topics look innocent at first glance, but they are not. You are expected to spend a sustained amount of time, thought, and exploration—about the same time that you spent on part 1.

Each topic will require you to carry out some exploratory coding. You should include the highlights of your code in your report, as appropriate, but you need not turn in any programs.

Each part's writeup should be at least two pages long.

a) I provided an AbstractBoard class for your convenience. Which of the other interfaces from the framework could benefit from such a convenience class? Provide those implementations that you find beneficial and explain why the others are not worth doing. Discuss why the framework author did not supply any of these convenience classes.

b) Discuss the method

IPiece[][] getBoard()

of the IBoard interface. Where is the method used. What assumptions does the framework author make about the game? Are those assumptions fulfilled in the case of Flip? Develop a more extensible design and show how it can be used in Flip and TicTacToe.

c) It is considered poor style to use an iterator as a "poor man's collection". For example, this FAQ entry recommends that you pass around collection objects, not iterator objects. After all, you can always get the iterator from the collection (and do it again if you want to iterate twice). Yet the game framework uses iterators in several places. Analyze which of them could have been replaced by collections, and which could not, and why.

d) Since there are two kinds of moves, it makes sense to provide two classes Flip and Play. (Did you do that in your implementation? If not, why not?) Then there is a challenge in the doMove and undoMove methods—they receive IMove instances, and it would be highly inelegant to make an instanceof test. Discuss how you can make use of the Strategy design pattern to solve this issue. How could the IMove interface be improved to facilitate the Strategy pattern?

e) The FlipFrame and FlipPanel classes implement a GUI for the Flip game. You don't need to understand the implementation of either class, and you need not look into the FlipPanel code at all. Produce a GUIPlayer that works similar to the ConsolePlayer. However, instead of printing the game state to the console, call frame.updateDisplay. Instead of getting console input, call frame.getInputString. The GUI classes translate mouse clicks into input strings. For example, if the user clicks on dice 1, 1, and 2 in the middle, then on the human player's die 4, the string "112f4" is returned.

You will also need to override the method

IPiece[][] getBoard()

to return a “ragged” array of six arrays of Die objects containing

Try out the GUIPlayer. It is almost a complete replacement for the ConsolePlayer, but how does it fall short? What change should be made to the framework to make the GUIPlayer work perfectly? How would that change benefit the framework for other player interaction (e.g. play across a network)?