Activity 8: Implementing Elevens Board

Introduction

In Activity 8, we discussed refactoring (reorganizing) the original ElevensBoard class into a new Board class and a much smaller ElevensBoard class. The purpose of the change was to allow code reuse in new games such as Tens and Thirteens. Now you will complete the implementation of the methods in the refactored ElevensBoard class.


Exploration

Copy the Board, CardGameGUI, ElevensGUIRunner, and the new ElevensBoard code from below into your project. You can replace the old ElevensBoard class.


Exercises

You are going to implement the following methods in the ElevensBoard class. Please note that when you find the method you are going to be implementing, there are comments written in there that will give you more information about how to implement it.

  1. isLegal
  2. anotherPlayPossible
  3. containsPairSum11 (helper method)
  4. containsJQK (helper method)
  5. Download this folder and put it in the same package as your other class files. That would be the 'bin' folder within your project folder in your workspace.

When you're done, run the main method in ElevensGUIRunner to make sure it works correctly.


Questions

  1. The size of the board is one of the differences between Elevens and Thirteens. Why is size not an abstract method?
  2. Why are there no abstract methods dealing with the selection of the cards to be removed or replaced in the array cards?
  3. Another way to create an "IS-A" relationship is by implementing interfaces. Suppose that instead of creating an abstract Board class, we created the following Board interface and had ElevensBoardimplement it. Would this new scheme allow the Elevens GUI to call isLegal and anotherPlayIsPossible polymorphically? In general, would this set-up be as good as the abstract class set-up? Why or why not?



Board.java

import java.util.List;
import java.util.ArrayList;

/**
 * This class represents a Board that can be used in a collection
 * of solitaire games similar to Elevens.  The variants differ in
 * card removal and the board size.
 */
public abstract class Board {

  /**
   * The cards on this board.
   */
  private Card[] cards;

  /**
   * The deck of cards being used to play the current game.
   */
  private Deck deck;

  /**
   * Flag used to control debugging print statements.
   */
  private static final boolean I_AM_DEBUGGING = false;

  /**
   * Creates a new <code>Board</code> instance.
   * @param size the number of cards in the board
   * @param ranks the names of the card ranks needed to create the deck
   * @param suits the names of the card suits needed to create the deck
   * @param pointValues the integer values of the cards needed to create
   *                    the deck
   */
  public Board(int size, String[] ranks, String[] suits, int[] pointValues) {
    cards = new Card[size];
    deck = new Deck(ranks, suits, pointValues);
    if (I_AM_DEBUGGING) {
      System.out.println(deck);
      System.out.println("----------");
    }
    dealMyCards();
  }

  /**
   * Start a new game by shuffling the deck and
   * dealing some cards to this board.
   */
  public void newGame() {
    deck.shuffle();
    dealMyCards();
  }

  /**
   * Accesses the size of the board.
   * Note that this is not the number of cards it contains,
   * which will be smaller near the end of a winning game.
   * @return the size of the board
   */
  public int size() {
    return cards.length;
  }

  /**
   * Determines if the board is empty (has no cards).
   * @return true if this board is empty; false otherwise.
   */
  public boolean isEmpty() {
    for (int k = 0; k < cards.length; k++) {
      if (cards[k] != null) {
        return false;
      }
    }
    return true;
  }

  /**
   * Deal a card to the kth position in this board.
   * If the deck is empty, the kth card is set to null.
   * @param k the index of the card to be dealt.
   */
  public void deal(int k) {
    cards[k] = deck.deal();
  }

  /**
   * Accesses the deck's size.
   * @return the number of undealt cards left in the deck.
   */
  public int deckSize() {
    return deck.size();
  }

  /**
   * Accesses a card on the board.
   * @return the card at position k on the board.
   * @param k is the board position of the card to return.
   */
  public Card cardAt(int k) {
    return cards[k];
  }

  /**
   * Replaces selected cards on the board by dealing new cards.
   * @param selectedCards is a list of the indices of the
   *        cards to be replaced.
   */
  public void replaceSelectedCards(List<Integer> selectedCards) {
    for (Integer k : selectedCards) {
      deal(k.intValue());
    }
  }

  /**
   * Gets the indexes of the actual (non-null) cards on the board.
   *
   * @return a List that contains the locations (indexes)
   *         of the non-null entries on the board.
   */
  public List<Integer> cardIndexes() {
    List<Integer> selected = new ArrayList<Integer>();
    for (int k = 0; k < cards.length; k++) {
      if (cards[k] != null) {
        selected.add(new Integer(k));
      }
    }
    return selected;
  }

  /**
   * Generates and returns a string representation of this board.
   * @return the string version of this board.
   */
  public String toString() {
    String s = "";
    for (int k = 0; k < cards.length; k++) {
      s = s + k + ": " + cards[k] + "\n";
    }
    return s;
  }

  /**
   * Determine whether or not the game has been won,
   * i.e. neither the board nor the deck has any more cards.
   * @return true when the current game has been won;
   *         false otherwise.
   */
  public boolean gameIsWon() {
    if (deck.isEmpty()) {
      for (Card c : cards) {
        if (c != null) {
          return false;
        }
      }
      return true;
    }
    return false;
  }

  /**
   * Method to be completed by the concrete class that determines
   * if the selected cards form a valid group for removal.
   * @param selectedCards the list of the indices of the selected cards.
   * @return true if the selected cards form a valid group for removal;
   *         false otherwise.
   */
  public abstract boolean isLegal(List<Integer> selectedCards);

  /**
   * Method to be completed by the concrete class that determines
   * if there are any legal plays left on the board.
   * @return true if there is a legal play left on the board;
   *         false otherwise.
   */
  public abstract boolean anotherPlayIsPossible();

  /**
   * Deal cards to this board to start the game.
   */
  private void dealMyCards() {
    for (int k = 0; k < cards.length; k++) {
      cards[k] = deck.deal();
    }
  }
}

ElevensBoard.java

import java.util.List;
import java.util.ArrayList;

/**
 * The ElevensBoard class represents the board in a game of Elevens.
 */
public class ElevensBoard extends Board {

  /**
   * The size (number of cards) on the board.
   */
  private static final int BOARD_SIZE = 9;

  /**
   * The ranks of the cards for this game to be sent to the deck.
   */
  private static final String[] RANKS =
    {"ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "jack", "queen", "king"};

  /**
   * The suits of the cards for this game to be sent to the deck.
   */
  private static final String[] SUITS =
    {"spades", "hearts", "diamonds", "clubs"};

  /**
   * The values of the cards for this game to be sent to the deck.
   */
  private static final int[] POINT_VALUES =
    {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0};

  /**
   * Flag used to control debugging print statements.
   */
  private static final boolean I_AM_DEBUGGING = false;


  /**
   * Creates a new <code>ElevensBoard</code> instance.
   */
   public ElevensBoard() {
     super(BOARD_SIZE, RANKS, SUITS, POINT_VALUES);
   }

  /**
   * Determines if the selected cards form a valid group for removal.
   * In Elevens, the legal groups are (1) a pair of non-face cards
   * whose values add to 11, and (2) a group of three cards consisting of
   * a jack, a queen, and a king in some order.
   * @param selectedCards the list of the indices of the selected cards.
   * @return true if the selected cards form a valid group for removal;
   *         false otherwise.
   */
  @Override
  public boolean isLegal(List<Integer> selectedCards) {
    /* *** TO BE IMPLEMENTED IN ACTIVITY 9 *** */
  }

  /**
   * Determine if there are any legal plays left on the board.
   * In Elevens, there is a legal play if the board contains
   * (1) a pair of non-face cards whose values add to 11, or (2) a group
   * of three cards consisting of a jack, a queen, and a king in some order.
   * @return true if there is a legal play left on the board;
   *         false otherwise.
   */
  @Override
  public boolean anotherPlayIsPossible() {
    /* *** TO BE IMPLEMENTED IN ACTIVITY 9 *** */
  }

  /**
   * Check for an 11-pair in the selected cards.
   * @param selectedCards selects a subset of this board.  It is list
   *                      of indexes into this board that are searched
   *                      to find an 11-pair.
   * @return true if the board entries in selectedCards
   *              contain an 11-pair; false otherwise.
   */
  private boolean containsPairSum11(List<Integer> selectedCards) {
    /* *** TO BE IMPLEMENTED IN ACTIVITY 9 *** */
  }

  /**
   * Check for a JQK in the selected cards.
   * @param selectedCards selects a subset of this board.  It is list
   *                      of indexes into this board that are searched
   *                      to find a JQK group.
   * @return true if the board entries in selectedCards
   *              include a jack, a queen, and a king; false otherwise.
   */
  private boolean containsJQK(List<Integer> selectedCards) {
    /* *** TO BE IMPLEMENTED IN ACTIVITY 9 *** */
  }
}

ElevensGUIRunner.java

/**
 * This is a class that plays the GUI version of the Elevens game.
 * See accompanying documents for a description of how Elevens is played.
 */
public class ElevensGUIRunner {

  /**
   * Plays the GUI version of Elevens.
   * @param args is not used.
   */
  public static void main(String[] args) {
    Board board = new ElevensBoard();
    CardGameGUI gui = new CardGameGUI(board);
    gui.displayGame();
  }
}

CardGameGUI.java

import java.awt.Point;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Color;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.ImageIcon;
import java.net.URL;
import java.util.List;
import java.util.ArrayList;

/**
 * This class provides a GUI for solitaire games related to Elevens.
 */
public class CardGameGUI extends JFrame implements ActionListener {

  /** Height of the game frame. */
  private static final int DEFAULT_HEIGHT = 302;
  /** Width of the game frame. */
  private static final int DEFAULT_WIDTH = 800;
  /** Width of a card. */
  private static final int CARD_WIDTH = 73;
  /** Height of a card. */
  private static final int CARD_HEIGHT = 97;
  /** Row (y coord) of the upper left corner of the first card. */
  private static final int LAYOUT_TOP = 30;
  /** Column (x coord) of the upper left corner of the first card. */
  private static final int LAYOUT_LEFT = 30;
  /** Distance between the upper left x coords of
   *  two horizonally adjacent cards. */
  private static final int LAYOUT_WIDTH_INC = 100;
  /** Distance between the upper left y coords of
   *  two vertically adjacent cards. */
  private static final int LAYOUT_HEIGHT_INC = 125;
  /** y coord of the "Replace" button. */
  private static final int BUTTON_TOP = 30;
  /** x coord of the "Replace" button. */
  private static final int BUTTON_LEFT = 570;
  /** Distance between the tops of the "Replace" and "Restart" buttons. */
  private static final int BUTTON_HEIGHT_INC = 50;
  /** y coord of the "n undealt cards remain" label. */
  private static final int LABEL_TOP = 160;
  /** x coord of the "n undealt cards remain" label. */
  private static final int LABEL_LEFT = 540;
  /** Distance between the tops of the "n undealt cards" and
   *  the "You lose/win" labels. */
  private static final int LABEL_HEIGHT_INC = 35;

  /** The board (Board subclass). */
  private Board board;

  /** The main panel containing the game components. */
  private JPanel panel;
  /** The Replace button. */
  private JButton replaceButton;
  /** The Restart button. */
  private JButton restartButton;
  /** The "number of undealt cards remain" message. */
  private JLabel statusMsg;
  /** The "you've won n out of m games" message. */
  private JLabel totalsMsg;
  /** The card displays. */
  private JLabel[] displayCards;
  /** The win message. */
  private JLabel winMsg;
  /** The loss message. */
  private JLabel lossMsg;
  /** The coordinates of the card displays. */
  private Point[] cardCoords;

  /** kth element is true iff the user has selected card #k. */
  private boolean[] selections;
  /** The number of games won. */
  private int totalWins;
  /** The number of games played. */
  private int totalGames;


  /**
   * Initialize the GUI.
   * @param gameBoard is a <code>Board</code> subclass.
   */
  public CardGameGUI(Board gameBoard) {
    board = gameBoard;
    totalWins = 0;
    totalGames = 0;

    // Initialize cardCoords using 5 cards per row
    cardCoords = new Point[board.size()];
    int x = LAYOUT_LEFT;
    int y = LAYOUT_TOP;
    for (int i = 0; i < cardCoords.length; i++) {
      cardCoords[i] = new Point(x, y);
      if (i % 5 == 4) {
        x = LAYOUT_LEFT;
        y += LAYOUT_HEIGHT_INC;
      } else {
        x += LAYOUT_WIDTH_INC;
      }
    }

    selections = new boolean[board.size()];
    initDisplay();
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    repaint();
  }

  /**
   * Run the game.
   */
  public void displayGame() {
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        setVisible(true);
      }
    });
  }

  /**
   * Draw the display (cards and messages).
   */
  public void repaint() {
    for (int k = 0; k < board.size(); k++) {
      String cardImageFileName =
        imageFileName(board.cardAt(k), selections[k]);
      URL imageURL = getClass().getResource(cardImageFileName);
      if (imageURL != null) {
        ImageIcon icon = new ImageIcon(imageURL);
        displayCards[k].setIcon(icon);
        displayCards[k].setVisible(true);
      } else {
        throw new RuntimeException(
          "Card image not found: \"" + cardImageFileName + "\"");
      }
    }
    statusMsg.setText(board.deckSize()
      + " undealt cards remain.");
    statusMsg.setVisible(true);
    totalsMsg.setText("You've won " + totalWins
       + " out of " + totalGames + " games.");
    totalsMsg.setVisible(true);
    pack();
    panel.repaint();
  }

  /**
   * Initialize the display.
   */
  private void initDisplay()  {
    panel = new JPanel() {
      public void paintComponent(Graphics g) {
        super.paintComponent(g);
      }
    };

    // If board object's class name follows the standard format
    // of ...Board or ...board, use the prefix for the JFrame title
    String className = board.getClass().getSimpleName();
    int classNameLen = className.length();
    int boardLen = "Board".length();
    String boardStr = className.substring(classNameLen - boardLen);
    if (boardStr.equals("Board") || boardStr.equals("board")) {
      int titleLength = classNameLen - boardLen;
      setTitle(className.substring(0, titleLength));
    }

    // Calculate number of rows of cards (5 cards per row)
    // and adjust JFrame height if necessary
    int numCardRows = (board.size() + 4) / 5;
    int height = DEFAULT_HEIGHT;
    if (numCardRows > 2) {
      height += (numCardRows - 2) * LAYOUT_HEIGHT_INC;
    }

    this.setSize(new Dimension(DEFAULT_WIDTH, height));
    panel.setLayout(null);
    panel.setPreferredSize(
      new Dimension(DEFAULT_WIDTH - 20, height - 20));
    displayCards = new JLabel[board.size()];
    for (int k = 0; k < board.size(); k++) {
      displayCards[k] = new JLabel();
      panel.add(displayCards[k]);
      displayCards[k].setBounds(cardCoords[k].x, cardCoords[k].y,
                    CARD_WIDTH, CARD_HEIGHT);
      displayCards[k].addMouseListener(new MyMouseListener());
      selections[k] = false;
    }
    replaceButton = new JButton();
    replaceButton.setText("Replace");
    panel.add(replaceButton);
    replaceButton.setBounds(BUTTON_LEFT, BUTTON_TOP, 100, 30);
    replaceButton.addActionListener(this);

    restartButton = new JButton();
    restartButton.setText("Restart");
    panel.add(restartButton);
    restartButton.setBounds(BUTTON_LEFT, BUTTON_TOP + BUTTON_HEIGHT_INC,
                    100, 30);
    restartButton.addActionListener(this);

    statusMsg = new JLabel(
      board.deckSize() + " undealt cards remain.");
    panel.add(statusMsg);
    statusMsg.setBounds(LABEL_LEFT, LABEL_TOP, 250, 30);

    winMsg = new JLabel();
    winMsg.setBounds(LABEL_LEFT, LABEL_TOP + LABEL_HEIGHT_INC, 200, 30);
    winMsg.setFont(new Font("SansSerif", Font.BOLD, 25));
    winMsg.setForeground(Color.GREEN);
    winMsg.setText("You win!");
    panel.add(winMsg);
    winMsg.setVisible(false);

    lossMsg = new JLabel();
    lossMsg.setBounds(LABEL_LEFT, LABEL_TOP + LABEL_HEIGHT_INC, 200, 30);
    lossMsg.setFont(new Font("SanSerif", Font.BOLD, 25));
    lossMsg.setForeground(Color.RED);
    lossMsg.setText("Sorry, you lose.");
    panel.add(lossMsg);
    lossMsg.setVisible(false);

    totalsMsg = new JLabel("You've won " + totalWins
      + " out of " + totalGames + " games.");
    totalsMsg.setBounds(LABEL_LEFT, LABEL_TOP + 2 * LABEL_HEIGHT_INC,
                  250, 30);
    panel.add(totalsMsg);

    if (!board.anotherPlayIsPossible()) {
      signalLoss();
    }

    pack();
    getContentPane().add(panel);
    getRootPane().setDefaultButton(replaceButton);
    panel.setVisible(true);
  }

  /**
   * Deal with the user clicking on something other than a button or a card.
   */
  private void signalError() {
    Toolkit t = panel.getToolkit();
    t.beep();
  }

  /**
   * Returns the image that corresponds to the input card.
   * Image names have the format "[Rank][Suit].GIF" or "[Rank][Suit]S.GIF",
   * for example "aceclubs.GIF" or "8heartsS.GIF". The "S" indicates that
   * the card is selected.
   *
   * @param c Card to get the image for
   * @param isSelected flag that indicates if the card is selected
   * @return String representation of the image
   */
  private String imageFileName(Card c, boolean isSelected) {
    String str = "cards/";
    if (c == null) {
      return "cards/back1.GIF";
    }
    str += c.rank() + c.suit();
    if (isSelected) {
      str += "S";
    }
    str += ".GIF";
    return str;
  }

  /**
   * Respond to a button click (on either the "Replace" button
   * or the "Restart" button).
   * @param e the button click action event
   */
  public void actionPerformed(ActionEvent e) {
    if (e.getSource().equals(replaceButton)) {
      // Gather all the selected cards.
      List<Integer> selection = new ArrayList<Integer>();
      for (int k = 0; k < board.size(); k++) {
        if (selections[k]) {
          selection.add(new Integer(k));
        }
      }
      // Make sure that the selected cards represent a legal replacement.
      if (!board.isLegal(selection)) {
        signalError();
        return;
      }
      for (int k = 0; k < board.size(); k++) {
        selections[k] = false;
      }
      // Do the replace.
      board.replaceSelectedCards(selection);
      if (board.isEmpty()) {
        signalWin();
      } else if (!board.anotherPlayIsPossible()) {
        signalLoss();
      }
      repaint();
    } else if (e.getSource().equals(restartButton)) {
      board.newGame();
      getRootPane().setDefaultButton(replaceButton);
      winMsg.setVisible(false);
      lossMsg.setVisible(false);
      if (!board.anotherPlayIsPossible()) {
        signalLoss();
        lossMsg.setVisible(true);
      }
      for (int i = 0; i < selections.length; i++) {
        selections[i] = false;
      }
      repaint();
    } else {
      signalError();
      return;
    }
  }

  /**
   * Display a win.
   */
  private void signalWin() {
    getRootPane().setDefaultButton(restartButton);
    winMsg.setVisible(true);
    totalWins++;
    totalGames++;
  }

  /**
   * Display a loss.
   */
  private void signalLoss() {
    getRootPane().setDefaultButton(restartButton);
    lossMsg.setVisible(true);
    totalGames++;
  }

  /**
   * Receives and handles mouse clicks.  Other mouse events are ignored.
   */
  private class MyMouseListener implements MouseListener {

    /**
     * Handle a mouse click on a card by toggling its "selected" property.
     * Each card is represented as a label.
     * @param e the mouse event.
     */
    public void mouseClicked(MouseEvent e) {
      for (int k = 0; k < board.size(); k++) {
        if (e.getSource().equals(displayCards[k])
            && board.cardAt(k) != null) {
          selections[k] = !selections[k];
          repaint();
          return;
        }
      }
      signalError();
    }

    /**
     * Ignore a mouse exited event.
     * @param e the mouse event.
     */
    public void mouseExited(MouseEvent e) {
    }

    /**
     * Ignore a mouse released event.
     * @param e the mouse event.
     */
    public void mouseReleased(MouseEvent e) {
    }

    /**
     * Ignore a mouse entered event.
     * @param e the mouse event.
     */
    public void mouseEntered(MouseEvent e) {
    }

    /**
     * Ignore a mouse pressed event.
     * @param e the mouse event.
     */
    public void mousePressed(MouseEvent e) {
    }
  }
}