The following code simulates the card game Crazy Eights. Some of the methods seem a little clunky or superfluous (although I've yet to come up with a better solution). I'm specifically referring to the convert
method used in EightsCard
which is called as a helper method in a number of EightsPlayer
methods (play
, searchForMatch
and drawForMatch
).
I'm really interested in way that this code could be refactored. After spending some time reading up on abstract classes and interfaces I'm starting to think that the classes in the 'defaults' folder would be better off as interfaces with default
methods.
What I want is a good set of default classes and/or interfaces which I can use as a starting point for virtually any classic playing card game.
Card
:
public class Card {
public static final String[] RANKS = {
null, "Ace", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "Jack", "Queen", "King"};
public static final String[] SUITS = {
"Clubs", "Diamonds", "Hearts", "Spades"};
private final int rank;
private final int suit;
public Card(int rank, int suit) {
this.rank = rank;
this.suit = suit;
}
public int getRank() {
return rank;
}
public int getSuit() {
return suit;
}
public String toString() {
return RANKS[rank] + " of " + SUITS[suit];
}
public boolean equals(Card that) {
return rank == that.rank
&& suit == that.suit;
}
/**
* Compares Card values when Aces are Low and the suit value dominates.
* @param that Card to be compared
* @return -1 if lower value, 1 otherwise.
*/
public int compareValueALSD(Card that) {
if (suit < that.suit) {
return -1;
}
if (suit > that.suit) {
return 1;
}
if (rank < that.rank) {
return -1;
}
if (rank > that.rank) {
return 1;
}
return 0;
}
/**
* Compares Card values when Aces are High and the suit value dominates.
* @param that Card to be compared
* @return -1 if lower value, 1 otherwise.
*/
public int compareValueAHSD(Card that) {
if (suit < that.suit) {
return -1;
}
if (suit > that.suit) {
return 1;
}
if (this.rank == 1 && that.rank != 1) {
return 1;
}
if (rank < that.rank) {
return -1;
}
if (rank > that.rank) {
return 1;
}
return 0;
}
/**
* Prints a deck of cards.
*/
public static void printDeck(Card[] cards) {
for (int i = 0; i < cards.length; i++) {
System.out.println(cards[i]);
}
}
}
CardCollection
:
import java.util.ArrayList;
import java.util.Random;
/**
* A collection of playing cards.
*/
public class CardCollection {
private String label;
private ArrayList<Card> cards;
/**
* Constructs an empty collection.
*/
public CardCollection(String label) {
this.label = label;
this.cards = new ArrayList<Card>();
}
/**
* Returns the label of the card collection.
*/
public String getLabel() {
return label;
}
/**
* Returns the card with the given index.
*/
public Card getCard(int i) {
return cards.get(i);
}
/**
* Adds the given card to the collection.
*/
public void addCard(Card card) {
cards.add(card);
}
/**
* Removes and returns the card with the given index.
*/
public Card popCard(int i) {
return cards.remove(i);
}
/**
* Removes and returns the last card.
*/
public Card popCard() {
int i = size() - 1;
return popCard(i);
}
/**
* Returns the number of cards.
*/
public int size() {
return cards.size();
}
/**
* True if the collection is empty, false otherwise.
*/
public boolean empty() {
return cards.size() == 0;
}
/**
* Moves n cards from this collection to the given collection.
*/
public void deal(CardCollection that, int n) {
for (int i = 0; i < n; i++) {
Card card = popCard();
that.addCard(card);
}
}
/**
* Moves all remaining cards to the given collection.
*/
public void dealAll(CardCollection that) {
int n = size();
deal(that, n);
}
/**
* Returns the last card.
*/
public Card last() {
int i = size() - 1;
return cards.get(i);
}
/**
* Swaps the cards at indexes i and j.
*/
public void swapCards(int i, int j) {
Card temp = cards.get(i);
cards.set(i, cards.get(j));
cards.set(j, temp);
}
/**
* Randomly permute the cards.
*/
public void shuffle() {
Random random = new Random();
for (int i = size() - 1; i > 0; i--) {
int j = random.nextInt(i);
swapCards(i, j);
}
}
/**
* Returns a string representation of the card collection.
*/
public String toString() {
return label + ": " + cards.toString();
}
/**
* Prints the label and cards.
*/
public void display() {
System.out.println(label + ": ");
for (Card card: cards) {
System.out.println(card);
}
System.out.println();
}
}
Deck
:
/**
* A deck of playing cards.
*/
public class Deck extends CardCollection {
/**
* Constructs a standard deck of 52 cards.
*/
public Deck(String label) {
super(label);
for (int suit = 0; suit <= 3; suit++) {
for (int rank = 1; rank <= 13; rank++) {
addCard(new Card(rank, suit));
}
}
}
}
Player
:
public class Player {
private String name;
private CardCollection hand;
/**
* Constructs a player with an empty hand.
*/
public Player(String name) {
this.name = name;
this.hand = new CardCollection(name);
}
/**
* Gets the player's name.
*/
public String getName() {
return name;
}
/**
* Gets the player's hand.
*/
public CardCollection getHand() {
return hand;
}
/**
* Displays the player's hand.
*/
public void display() {
hand.display();
}
Eights
:
import java.util.ArrayList;
import java.util.Scanner;
/**
* Simulates a game of Crazy Eights. See https://en.wikipedia.org/wiki/Crazy_Eights for basic play
* and scoring rules.
*/
public class Eights {
private CardCollection drawPile;
private CardCollection discardPile;
private Scanner in;
private ArrayList<EightsPlayer> players;
/**
* Initializes the state of the game.
*/
public Eights() {
Deck deck = new Deck("Deck");
deck.shuffle();
// create an ArrayList of players
this.players = new ArrayList<EightsPlayer>();
// turn one card face up
discardPile = new CardCollection("Discards");
deck.deal(discardPile, 1);
// put the rest of the deck face down
drawPile = new CardCollection("Draw pile");
deck.dealAll(drawPile);
// create the scanner we'll use to wait for the user
in = new Scanner(System.in);
}
/**
* Adds the given player to the collection.
*/
public void createPlayer(EightsPlayer player) {
players.add(player);
}
/**
* Prompts user to create a player name and uses input to create it.
* @return The player's name
*/
public String playerName() {
System.out.println("Enter player name: ");
String name = in.next();
while (name == "") {
name = in.nextLine();
}
return name;
}
/**
* Gives player within collection a name and a hand with 5 cards.
*/
public void equipPlayer() {
EightsPlayer player = new EightsPlayer(playerName());
createPlayer(player);
drawPile.deal(player.getHand(), 5);
}
/**
* Returns a player from the array at a given index.
*/
public EightsPlayer getPlayer(int i) {
return players.get(i);
}
/**
* Returns index of a given player.
*/
public int getPlayerIndex(EightsPlayer player) {
return players.indexOf(player);
}
/**
* Returns true if either hand is empty.
*/
public boolean isDone() {
for (int x = 0; x < players.size(); x++) {
if (getPlayer(x).getHand().empty()) {
return true;
}
}
return false;
}
/**
* Moves cards from the discard pile to the draw pile and shuffles.
*/
public void reshuffle() {
// save the top card
Card convert = discardPile.popCard();
EightsCard prev = new EightsCard(0, 0);
prev = prev.convert(convert);
// move the rest of the cards
discardPile.dealAll(drawPile);
// put the top card back
discardPile.addCard(prev);
// shuffle the draw pile
drawPile.shuffle();
}
/**
* Returns a card from the draw pile.
*/
public EightsCard draw() {
if (drawPile.empty()) {
reshuffle();
}
Card convert = drawPile.popCard();
EightsCard prev = new EightsCard(0, 0);
return prev.convert(convert);
}
/**
* Switches players.
*/
public EightsPlayer nextPlayer(EightsPlayer current) {
if (getPlayerIndex(current) < players.size() - 1) {
return getPlayer(getPlayerIndex(current) + 1);
}
if (getPlayerIndex(current) == players.size() - 1) {
return getPlayer(getPlayerIndex(current) + 1 - players.size());
}
return null;
}
/**
* Displays the state of the game.
*/
public void displayState() {
for (int x = 0; x < players.size(); x++) {
getPlayer(x).display();
}
discardPile.display();
System.out.print("Draw pile: ");
System.out.println(drawPile.size() + " cards");
}
/**
* Waits for the user to press enter.
*/
public void waitForUser() {
in.nextLine();
}
/**
* One player takes a turn.
*/
public void takeTurn(EightsPlayer player) {
Card convert = discardPile.last();
EightsCard prev = new EightsCard(0, 0);
prev = prev.convert(convert);
EightsCard next = player.play(this, prev);
discardPile.addCard(next);
System.out.println(player.getName() + " plays " + next);
System.out.println();
}
/**
* Plays the game.
*/
public void playGame() {
System.out.println("It's time to play Crazy Eights!");
System.out.println();
System.out.println("Enter number of players: ");
int nop = in.nextInt();
while (nop > 4 || nop < 2) {
System.err.println("Number of players must be between 2 and 4!");
System.out.println();
System.out.println("Re-enter number of players: ");
nop = in.nextInt();
}
for (int i = 0; i < nop; i++) {
equipPlayer();
}
EightsPlayer player = getPlayer(0);
// keep playing until there's a winner
while (!isDone()) {
displayState();
waitForUser();
takeTurn(player);
player = nextPlayer(player);
}
// display the final score
for (int x = 0; x < players.size(); x++) {
getPlayer(x).displayScore();
}
}
/**
* Creates the game and runs it.
*/
public static void main(String[] args) {
Eights game = new Eights();
game.playGame();
}
}
EightsCard
:
public class EightsCard extends Card {
public EightsCard(int rank, int suit) {
super(rank, suit);
}
/**
* Checks whether cards match.
*/
public boolean cardMatches(EightsCard that) {
if (getSuit() == that.getSuit()) {
return true;
}
if (getRank() == that.getRank()) {
return true;
}
if (getRank() == 8) {
return true;
}
return false;
}
/**
* Gives the card score for a given card.
* @return card score
*/
public int scoreCard() {
int rank = getRank();
if (rank == 8) {
return -20;
} else if (rank > 10) {
return -10;
} else {
return -rank;
}
}
/**
* Converts a Card into an EightsCard
* @param card The card to be converted
* @return The converted EightsCard
*/
public EightsCard convert(Card card) {
if (card != null) {
EightsCard result = new EightsCard(card.getRank(), card.getSuit());
return result;
}
return null;
}
}
EightsPlayer
:
public class EightsPlayer extends Player {
private CardCollection eightsHand;
public EightsPlayer(String name) {
super(name);
eightsHand = new CardCollection(name);
}
public CardCollection getHand() {
return eightsHand;
}
/**
* Removes and returns a legal card from the player's hand.
*/
public EightsCard play(Eights eights, EightsCard prev) {
EightsCard ecard = new EightsCard(0, 0);
ecard = ecard.convert(searchForMatch(prev));
if (ecard == null) {
ecard = drawForMatch(eights, prev);
return ecard;
}
return ecard;
}
/**
* Searches the player's hand for a matching card.
*/
public Card searchForMatch(EightsCard prev) {
for (int i = 0; i < eightsHand.size(); i++) {
Card card = eightsHand.getCard(i);
EightsCard ecard = new EightsCard(0,0);
ecard = ecard.convert(card);
if (ecard.cardMatches(prev)) {
return eightsHand.popCard(i);
}
}
return null;
}
/**
* Draws cards until a match is found.
*/
public EightsCard drawForMatch(Eights eights, EightsCard prev) {
while (true) {
EightsCard card = eights.draw();
System.out.println(getName() + " draws " + card);
if (card.cardMatches(prev)) {
return card;
}
eightsHand.addCard(card);
}
}
/**
* Calculates the player's score (penalty points).
*/
public int score() {
int sum = 0;
for (int i = 0; i < eightsHand.size(); i++) {
Card card = eightsHand.getCard(i);
int rank = card.getRank();
if (rank == 8) {
sum -= 20;
} else if (rank > 10) {
sum -= 10;
} else {
sum -= rank;
}
}
return sum;
}
/**
* Displays the player's name and score.
*/
public void displayScore() {
System.out.println(getName() + " has " + score() + " points");
}
/**
* Displays the player's hand.
*/
public void display() {
getHand().display();
}
}
Here is another version of the game which uses an extra class EightsCardCollection
but avoids the clunky convert method:
You can also see the UML class diagrams associated with these two classes over on Stack Overflow.