Checkers, anyone?

Develop a Swing-based library that presents a checkers game user interface.

checkers swing ui
Credit: Philip Taylor, Flickr

Several months ago, I was asked to create a small Java library that can be accessed by an application to render a graphical user interface (GUI) for the game of Checkers. As well as rendering a checkerboard and checkers, the GUI must allow a checker to be dragged from one square to another. Also, a checker must be centered on a square and must not be assigned to a square that's occupied by another checker. In this post, I present my library.

Designing a checkers GUI library

What public types should the library support? In checkers, each of two players alternately moves one of its regular (non-king) checkers over a board in a forward direction only and possibly jumps the other player's checker(s). When the checker reaches the other side, it's promoted to a king, which can also move in a backwards direction. From this description, we can infer the following types:

  • Board
  • Checker
  • CheckerType
  • Player

A Board object identifies the checkerboard. It serves as a container for Checker objects that occupy various squares. It can draw itself and request that each contained Checker object draw itself.

A Checker object identifies a checker. It has a color and an indication of whether it's a regular checker or a king checker. It can draw itself and makes its size available to Board, whose size is influenced by the Checker size.

CheckerType is an enum that identifies a checker color and type via its four constants: BLACK_KING, BLACK_REGULAR, RED_KING, and RED_REGULAR.

A Player object is a controller for moving a checker with optional jumps. Because I've chosen to implement this game in Swing, Player isn't necessary. Instead, I've turned Board into a Swing component whose constructor registers mouse and mouse-motion listeners that handle checker movement on behalf of the human player. In the future, I could implement a computer player via another thread, a synchronizer, and another Board method (such as move()).

What public APIs do Board and Checker contribute? After some thought, I came up with the following public Board API:

  • Board(): Construct a Board object. The constructor performs various initialization tasks such as listener registration.
  • void add(Checker checker, int row, int column): Add checker to Board at the position identified by row and column. The row and column are 1-based values as opposed to being 0-based (see Figure 1). The add() throws java.lang.IllegalArgumentException when its row or column argument is less than 1 or greater than 8. Also, it throws the unchecked AlreadyOccupiedException when you try to add a Checker to an occupied square.
  • Dimension getPreferredSize(): Return the Board component's preferred size for layout purposes.

Figure 1. The checkboard's upper-left corner is located at (1, 1)

The checkboard's upper-left corner is located at (1, 1)

I also developed the following public Checker API:

  • Checker(CheckerType checkerType): Construct a Checker object of the specified checkerType (BLACK_KING, BLACK_REGULAR, RED_KING, or RED_REGULAR).
  • void draw(Graphics g, int cx, int cy): Draw a Checker using the specified graphics context g with the center of the checker located at (cx, cy). This method is intended to be called from Board only.
  • boolean contains(int x, int y, int cx, int cy): A static helper method called from Board that determines if mouse coordinates (x, y) lie inside the checker whose center coordinates are specified by (cx, cy) and whose dimension is specified elsewhere in the Checker class.
  • int getDimension(): A static helper method called from Board that determines the size of a checker so that the board can size its squares and overall size appropriately.

This pretty much covers all of the checkers GUI library in terms of its types and their public APIs. We'll now focus on how I implemented this library.

Implementing the checkers GUI library

The checkers GUI library consists of four public types located in same-named source files: AlreadyOccupiedException, Board, Checker, and CheckerType. Listing 1 presents AlreadyOccupiedException's source code.

Listing 1. AlreadyOccupiedException.java

public class AlreadyOccupiedException extends RuntimeException
{
   public AlreadyOccupiedException(String msg)
   {
      super(msg);
   }
}

AlreadyOccupiedException extends java.lang.RuntimeException, which makes AlreadyOccupiedException an unchecked exception (it doesn't have to be caught or declared in a throws clause). If I wanted to make AlreadyOccupiedException checked, I would have extended java.lang.Exception. I chose to make this type unchecked because it operates similarly to the unchecked IllegalArgumentException.

AlreadyOccupiedException declares a constructor that takes a string argument describing the reason for the exception. This argument is forwarded to the RuntimeException superclass.

Listing 2 presents Board.

Listing 2. Board.java

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;

import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionAdapter;

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

import javax.swing.JComponent;

public class Board extends JComponent
{
   // dimension of checkerboard square (25% bigger than checker)

   private final static int SQUAREDIM = (int) (Checker.getDimension() * 1.25);

   // dimension of checkerboard (width of 8 squares)

   private final int BOARDDIM = 8 * SQUAREDIM;

   // preferred size of Board component

   private Dimension dimPrefSize;

   // dragging flag -- set to true when user presses mouse button over checker
   // and cleared to false when user releases mouse button

   private boolean inDrag = false;

   // displacement between drag start coordinates and checker center coordinates

   private int deltax, deltay;

   // reference to positioned checker at start of drag

   private PosCheck posCheck;

   // center location of checker at start of drag

   private int oldcx, oldcy;

   // list of Checker objects and their initial positions

   private List<PosCheck> posChecks;

   public Board()
   {
      posChecks = new ArrayList<>();
      dimPrefSize = new Dimension(BOARDDIM, BOARDDIM);

      addMouseListener(new MouseAdapter()
                       {
                          @Override
                          public void mousePressed(MouseEvent me)
                          {
                             // Obtain mouse coordinates at time of press.

                             int x = me.getX();
                             int y = me.getY();

                             // Locate positioned checker under mouse press.

                             for (PosCheck posCheck: posChecks)
                                if (Checker.contains(x, y, posCheck.cx, 
                                                     posCheck.cy))
                                {
                                   Board.this.posCheck = posCheck;
                                   oldcx = posCheck.cx;
                                   oldcy = posCheck.cy;
                                   deltax = x - posCheck.cx;
                                   deltay = y - posCheck.cy;
                                   inDrag = true;
                                   return;
                                }
                          }

                          @Override
                          public void mouseReleased(MouseEvent me)
                          {
                             // When mouse released, clear inDrag (to
                             // indicate no drag in progress) if inDrag is
                             // already set.

                             if (inDrag)
                                inDrag = false;
                             else
                                return;

                             // Snap checker to center of square.

                             int x = me.getX();
                             int y = me.getY();
                             posCheck.cx = (x - deltax) / SQUAREDIM * SQUAREDIM + 
                                           SQUAREDIM / 2;
                             posCheck.cy = (y - deltay) / SQUAREDIM * SQUAREDIM + 
                                           SQUAREDIM / 2;

                             // Do not move checker onto an occupied square.

                             for (PosCheck posCheck: posChecks)
                                if (posCheck != Board.this.posCheck && 
                                    posCheck.cx == Board.this.posCheck.cx &&
                                    posCheck.cy == Board.this.posCheck.cy)
                                {
                                   Board.this.posCheck.cx = oldcx;
                                   Board.this.posCheck.cy = oldcy;
                                }
                             posCheck = null;
                             repaint();
                          }
                       });

      // Attach a mouse motion listener to the applet. That listener listens
      // for mouse drag events.

      addMouseMotionListener(new MouseMotionAdapter()
                             {
                                @Override
                                public void mouseDragged(MouseEvent me)
                                {
                                   if (inDrag)
                                   {
                                      // Update location of checker center.

                                      posCheck.cx = me.getX() - deltax;
                                      posCheck.cy = me.getY() - deltay;
                                      repaint();
                                   }
                                }
                             });

   }

   public void add(Checker checker, int row, int col)
   {
      if (row < 1 || row > 8)
         throw new IllegalArgumentException("row out of range: " + row);
      if (col < 1 || col > 8)
         throw new IllegalArgumentException("col out of range: " + col);
      PosCheck posCheck = new PosCheck();
      posCheck.checker = checker;
      posCheck.cx = (col - 1) * SQUAREDIM + SQUAREDIM / 2;
      posCheck.cy = (row - 1) * SQUAREDIM + SQUAREDIM / 2;
      for (PosCheck _posCheck: posChecks)
         if (posCheck.cx == _posCheck.cx && posCheck.cy == _posCheck.cy)
            throw new AlreadyOccupiedException("square at (" + row + "," +
                                               col + ") is occupied");
      posChecks.add(posCheck);
   }

   @Override
   public Dimension getPreferredSize()
   {
      return dimPrefSize;
   }

   @Override
   protected void paintComponent(Graphics g)
   {
      paintCheckerBoard(g);
      for (PosCheck posCheck: posChecks)
         if (posCheck != Board.this.posCheck)
            posCheck.checker.draw(g, posCheck.cx, posCheck.cy);

      // Draw dragged checker last so that it appears over any underlying 
      // checker.

      if (posCheck != null)
         posCheck.checker.draw(g, posCheck.cx, posCheck.cy);
   }

   private void paintCheckerBoard(Graphics g)
   {
      ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                        RenderingHints.VALUE_ANTIALIAS_ON);

      // Paint checkerboard.

      for (int row = 0; row < 8; row++)
      {
         g.setColor(((row & 1) != 0) ? Color.BLACK : Color.WHITE);
         for (int col = 0; col < 8; col++)
         {
            g.fillRect(col * SQUAREDIM, row * SQUAREDIM, SQUAREDIM, SQUAREDIM);
            g.setColor((g.getColor() == Color.BLACK) ? Color.WHITE : Color.BLACK);
         }
      }
   }

   // positioned checker helper class

   private class PosCheck
   {
      public Checker checker;
      public int cx;
      public int cy;
   }
}

Board extends javax.swing.JComponent, which makes Board a Swing component. As such, you can directly add a Board component to a Swing application's content pane.

Board declares SQUAREDIM and BOARDDIM constants that identify the pixel dimensions of a square and the checkboard. When initializing SQUAREDIM, I invoke Checker.getDimension() instead of accessing an equivalent public Checker constant. Joshua Block answers why I do this in Item #30 (Use enums instead of int constants) of the second edition of his book, Effective Java: "Programs that use the int enum pattern are brittle. Because int enums are compile-time constants, they are compiled into the clients that use them. If the int associated with an enum constant is changed, its clients must be recompiled. If they aren’t, they will still run, but their behavior will be undefined."

Because of the extensive comments, I haven't much more to say about Board. However, note the nested PosCheck class, which describes a positioned checker by storing a Checker reference and its center coordinates, which are relative to the upper-left corner of the Board component. When you add a Checker object to the Board, it's stored in a new PosCheck object along with the center position of the checker, which is calculated from the specified row and column.

Listing 3 presents Checker.

1 2 Page 1
Notice to our Readers
We're now using social media to take your comments and feedback. Learn more about this here.