Checkers, anyone?

how-to
Dec 13, 201512 mins

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

checkers swing ui
Credit: Phillip Taylor

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.

Listing 3. Checker.java

import java.awt.Color;
import java.awt.Graphics;

public final class Checker
{
   private final static int DIMENSION = 50;

   private CheckerType checkerType;

   public Checker(CheckerType checkerType)
   {
      this.checkerType = checkerType;
   }

   public void draw(Graphics g, int cx, int cy)
   {
      int x = cx - DIMENSION / 2;
      int y = cy - DIMENSION / 2;

      // Set checker color.

      g.setColor(checkerType == CheckerType.BLACK_REGULAR ||
                 checkerType == CheckerType.BLACK_KING ? Color.BLACK : 
                 Color.RED);

      // Paint checker.

      g.fillOval(x, y, DIMENSION, DIMENSION);
      g.setColor(Color.WHITE);
      g.drawOval(x, y, DIMENSION, DIMENSION);

      if (checkerType == CheckerType.RED_KING || 
          checkerType == CheckerType.BLACK_KING)
         g.drawString("K", cx, cy);
   }

   public static boolean contains(int x, int y, int cx, int cy)
   {
      return (cx - x) * (cx - x) + (cy - y) * (cy - y) < DIMENSION / 2 * 
             DIMENSION / 2;
   }

   // The dimension is returned via a method rather than by accessing the
   // DIMENSION constant directly to avoid brittle code. If the constant was
   // accessed directly and I changed its value in Checker and recompiled only
   // this class, the old DIMENSION value would be accessed from external 
   // classes whereas the new DIMENSION value would be used in Checker. The
   // result would be erratic code.
   
   public static int getDimension()
   {
      return DIMENSION;
   }
}

As with Board, there isn’t much to say about Checker. In the draw() method, expressions int x = cx - DIMENSION / 2; and int y = cy - DIMENSION / 2; convert from a checker’s center coordinates to the coordinates of its upper-left corner. These coordinates are required by the subsequent fillOval() and drawOval() method calls as their first two arguments. Also, contains() uses the Pythagorean theorem to help it determine if an arbitrary point (x, y) lies within the circle centered at (cx, cy) and having a radius of DIMENSION / 2. Essentially, the difference between these points is determined and used by the Pythagorean equation to determine if (x, y) lies within the circle.

For completeness, Listing 4 presents CheckerType.

Listing 4. CheckerType.java

public enum CheckerType
{
   BLACK_REGULAR,
   BLACK_KING,
   RED_REGULAR,
   RED_KING
}

Demonstrating the checkers GUI library

Now that you’ve learned about the checkers GUI library’s design and implementation, you’ll probably want to try it out. I’ve created a short Checkers application that demonstrate the library. Listing 5 presents its source code.

Listing 5. Checkers.java

import java.awt.EventQueue;

import javax.swing.JFrame;

public class Checkers extends JFrame
{
   public Checkers(String title)
   {
      super(title);
      setDefaultCloseOperation(EXIT_ON_CLOSE);

      Board board = new Board();
      board.add(new Checker(CheckerType.RED_REGULAR), 4, 1);
      board.add(new Checker(CheckerType.BLACK_REGULAR), 6, 3);
      board.add(new Checker(CheckerType.RED_KING), 5, 6);
      setContentPane(board);

      pack();
      setVisible(true);
   }

   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         new Checkers("Checkers");
                      }
                   };
      EventQueue.invokeLater(r);
   }
}

Assuming that the library’s four source files are in the same directory as Checkers.java, compile Listing 5 as follows:

javac Checkers.java

Run the resulting application as follows:

java Checkers

Figure 2 shows the resulting user interface.

Figure 2. A king is distinguished by letter K

A king is distinguished by letter K

Drag each checker over the board. You’ll notice that the checker snaps to the middle of the destination square. Also, try dragging a checker onto an occupied square and observe what happens.

Conclusion

This post presented one implementation of a GUI library for a Java checkers game. In a different implementation, I might not record Checkers in a List<PosCheck> collection. Instead, it might be more efficient and lead to clearer code to store them in a Checker[8][8] table and provide translation code between row/column indexes and checker center coordinates. I’m interested in your comments on alternative library implementations.

I plan to revisit the checkers GUI library in a future post and introduce additional capabilities, such as adding a remove() method to the Board class for removing a Checker that’s been jumped. Also, I want to introduce logic into Board that a computer player would call to move a checker and possibly jump one or more intermediate checkers. And in a post even further into the future, I’d like to explore some algorithms for a computer player and introduce a complete checkers game.

download
Get the source code for this post’s applications. Created by Jeff Friesen for JavaWorld

The following software was used to develop the post’s code:

  • 64-bit JDK 7u6

The post’s code was tested on the following platform(s):

  • JVM on 64-bit Windows 7 SP1