If we think about how to play the game of Go Fish!, there are certain pieces of information that all players will keep track of, regardless of the strategy they use to try to win the game. Each player will have a hand of cards, as well as a number of books that they have already collected. They also share several actions that they will all have to take, such as adding and removing cards from their hand, and reporting their score at the end of the game to determine the winner. When thinking about object oriented programming, these shared pieces of information and behaviors are excellent candidates for an abstract parent class.
In this second part of the lab, we will create an AbtractPlayer
abstract class that implements the instance variables and methods that are shared by every type of player. In parts 3 and 4, we will later implement child classes called RandomPlayer
and UserPlayer
that implement different strategies for winning the game.
Inside your GitHub repository, you will find a file called AbstractPlayer.java
. This is where we will implement the AbstractPlayer
abstract class that implements the provided Player
interface. Open this file in Visual Studio Code to get started.
Given how much is shared between different types of Player
’s, the AbstractPlayer
abstract class will be able to implement most of the instance variables and methods needed for a Player object. First, the instance variables should include:
/** The {@link Player}'s hand of {@link Card}s. */
protected Card[] hand;
/** The number of books (i.e,, four-of-a-kind) the {@link Player} has acquired. */
protected int bookScore;
/** The {@link Player}'s number in the game. */
protected int playerNumber;
Here, we make all of the instance variables protected
instead of private
because the AbstractPlayer
will be inherited, and protected allows all child classes to access the instance variables of its parent class.
If you open up the Player.java
file in Visual Studio Code, you can see a list of all the methods (and Javadocs describing their purpose) needed to implement the Player
interface. We will want to implement all of the methods of the Player
interface inside the AbstractPlayer
class, except for the decideRank
method which will differ based on the game strategies used in different children classes. As such, the decideRank
method can be declared as abstract
in AbstractPlayer
the class so that it must be implemented instead by the children classes:
public abstract int decideRank();
Below, we describe what should be implemented for each of the methods in AbstractPlayer.java
.
The responsibility of the AbstractPlayer
constructor is to take in the player’s unique number (either 1 or 2) assigned by the game and save it in the playerNumber
instance variable, as well as create default values for the hand
(an empty array of length 0, signifying the player has no cards, yet) and bookScore
(0) instance variables.
The gainCards
method takes as an argument an array of Card
s which should be added to the player’s hand
instance variable. This will require creating a new Card[]
array, copying all the Card
s from the hand instance variable into the new array, then copying all the Card
s from the method argument cards
into the new array, and finally assigning the new array to the hand
instance variable. In future labs, we will see how this can be done differently with other data structures!
Hint
Be careful that your hand
instance variable always has a length
equal to the exact number of Card
s in the AbstractPlayer
’s hand. So if you add n
cards, then the new length
should be hand.length + n
. And later if you remove m
cards, then the new length
should be hand.length - m
.
The requestCards
method takes as an argument a rank of card requested by the opponent and should return an array of Card
s from the player’s hand
that match that rank. For example, if the opponent asks for cards with rank 2 and the player’s hand
instance variable consists of three 2’s and one 5, then the method should return an array of length three with those three 2’s.
Before returning, this method should also remove any returned cards from the player’s hand
instance variable. In the example of the previous paragraph, the player’s hand instance variable should only include the one 5 when the function returns since the three 2’s are being returned (and eventually given to the opponent by the game logic).
Hint
Removing cards from the player’s hand is also needed for the checkBooks
method described below. Rather than implementing the same idea twice, it might be useful (although not required) to create a helper method private Card[] removeCards(int rank)
that is responsible for this small bit of code, which can then be called by the other two methods.
Another shared functionality of both the requestCards
and checkBooks
methods is counting how many cards of a given rank the player holds in their hand. This could be another opportunity for a helper function protected int countCards(int rank)
that returns a count of many cards in the hand
instance variable have a given rank.
Note: we recommend using protected
for this helper function instead of private
since it might also be useful (but not required) for your UserPlayer
in Part 4.
Since we are using Card[]
arrays to store the collection of cards in the player’s hand, we will need to create a new array to store the remaining cards, then we can assign that new array to the hand
instance variable to make sure the player’s hand is updated (similar to how we made a new array for the hand
instance variable in the gainCards
method.)
If the player does not have any of the requested rank currently in their hand, then the method should just return an empty Card[]
array of length 0.
The checkBooks
method is responsible for determining whether the player has collected a book of cards (i.e., four-of-a-kind). If the player does have a book, it should remove those four cards from the player’s hand
instance variable and add 1 to their bookScore
instance variable.
These methods are simply accessors for the length of the hand
instance variable and the value of the bookScore
instance variable, respectively. They are needed since we made the instance variables protected
to protect them from being edited by the GoFish
class or other non-child classes.
Unfortunately, because AbstractPlayer
is an abstract class, it is difficult to test before we implement a child class. We will discuss in the next part how we can test this code, which you might want to read before completing this part. However, this shouldn’t stop you from saving your progress periodically to GitHub using the add
, commit
, and push
commands with git.