Sei sulla pagina 1di 10

Sign In | Register

About
JAVA Q&A JavaWorld's in-house tutor takes questions on
By Jeff Friesen, JavaWorld core Java programming topics in this blog
DEC 13, 2015 12:48 PM PT dedicated to Java beginners everywhere.

TUTORIAL
Checkers, anyone?
Develop a Swing-based library that presents a checkers game user interface.

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 identies 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 identies a checker. It has a color and an indication of whether it's a regular
Sign In | Register
checker or a king checker. It can draw itself and makes its size available to Board, whose size is
inuenced by the Checker size.

CheckerType is an enum that identies 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 identied 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)


Sign In | Register

I also developed the following public Checker API:

Checker(CheckerType checkerType): Construct a Checker object of the specied


checkerType (BLACK_KING, BLACK_REGULAR, RED_KING, or RED_REGULAR).

void draw(Graphics g, int cx, int cy): Draw a Checker using the specied
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 specied by (cx, cy) and whose dimension is specied 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 les:
AlreadyOccupiedException, Board, Checker, and CheckerType. Listing 1 presents
AlreadyOccupiedException's source code.
Listing 1. AlreadyOccupiedException.java
Sign In | Register

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; Sign In | Register
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;


Sign In | Register
// 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)
{
Sign In | Register
// 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)
Sign In | Register
{
// 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);
Sign In | Register
// 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
Sign In | Register
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 arent, they will still run, but their
behavior will be undened."

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 specied row and
column.

Listing 3 presents Checker.

1 2 NEXT

Copyright 2017 IDG Communications, Inc.

Potrebbero piacerti anche