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:
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
null
if the preceding move was not a "play")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.
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)?