Damas, alguien?

Hace varios meses, me pidieron que creara una pequeña biblioteca de Java a la que se pueda acceder mediante una aplicación para representar una interfaz gráfica de usuario (GUI) para el juego de damas. Además de representar un tablero de ajedrez y fichas, la GUI debe permitir que se arrastre una ficha de un cuadrado a otro. Además, una ficha debe estar centrada en un cuadrado y no debe asignarse a una casilla que esté ocupada por otra ficha. En este post, presento mi biblioteca.

Diseñar una biblioteca GUI de damas

¿Qué tipos de público debería apoyar la biblioteca? En las fichas, cada uno de los dos jugadores mueve alternativamente una de sus fichas regulares (que no son el rey) sobre un tablero en una dirección hacia adelante solamente y posiblemente salta las fichas del otro jugador. Cuando la ficha llega al otro lado, asciende a rey, que también puede moverse hacia atrás. De esta descripción, podemos inferir los siguientes tipos:

  • Board
  • Checker
  • CheckerType
  • Player

Un Boardobjeto identifica el tablero de ajedrez. Sirve como contenedor para Checkerobjetos que ocupan varios cuadrados. Puede dibujarse a sí mismo y solicitar que cada Checkerobjeto contenido se dibuje a sí mismo.

Un Checkerobjeto identifica a un verificador. Tiene un color y una indicación de si es un inspector regular o un inspector rey. Puede dibujarse a sí mismo y pone su tamaño a disposición de Board, cuyo tamaño está influenciado por el Checkertamaño.

CheckerTypees una enumeración que identifica un color corrector y el tipo a través de sus cuatro constantes: BLACK_KING, BLACK_REGULAR, RED_KING, y RED_REGULAR.

Un Playerobjeto es un controlador para mover una ficha con saltos opcionales. Porque elegí implementar este juego en Swing, Playerno es necesario. En su lugar, me he convertido Boarden un componente Swing cuyo constructor registra los oyentes del mouse y del movimiento del mouse que manejan el movimiento del corrector en nombre del jugador humano. En el futuro, podría implementar un reproductor de computadora a través de otro hilo, un sincronizador y otro Boardmétodo (como move()).

¿Qué API públicas hacen Boardy Checkercontribuyen? Después de pensarlo un poco, se me ocurrió la siguiente BoardAPI pública :

  • Board(): Construye un Boardobjeto. El constructor realiza varias tareas de inicialización, como el registro de escucha.
  • void add(Checker checker, int row, int column): Agregue checkera Boarden la posición identificada por rowy column. La fila y la columna son valores basados ​​en 1 en lugar de estar basados ​​en 0 (consulte la Figura 1). La add()arroja java.lang.IllegalArgumentExceptioncuando su argumento de fila o columna es menor que 1 o mayor que 8. Además, arroja la casilla sin marcar AlreadyOccupiedExceptioncuando intenta agregar a Checkera un cuadrado ocupado.
  • Dimension getPreferredSize(): Devuelve el Boardtamaño preferido del componente para fines de diseño.

Figura 1. La esquina superior izquierda del tablero se encuentra en (1, 1)

También desarrollé la siguiente CheckerAPI pública :

  • Checker(CheckerType checkerType): Construir un Checkerobjeto de la especificada checkerType( BLACK_KING, BLACK_REGULAR, RED_KING, o RED_REGULAR).
  • void draw(Graphics g, int cx, int cy): Dibuje un Checkerusando el contexto gráfico especificado gcon el centro del corrector ubicado en ( cx, cy). Este método está diseñado para ser llamado Boardsolo desde .
  • boolean contains(int x, int y, int cx, int cy): Un staticmétodo auxiliar llamado desde Boardque determina si las coordenadas del mouse ( x, y) se encuentran dentro del comprobador cuyas coordenadas centrales están especificadas por ( cx, cy) y cuya dimensión se especifica en otra parte de la Checkerclase.
  • int getDimension(): Un staticmétodo auxiliar llamado desde Boardque determina el tamaño de una ficha para que el tablero pueda dimensionar sus cuadrados y el tamaño total de manera apropiada.

Esto cubre prácticamente toda la biblioteca de la GUI de Checkers en términos de sus tipos y sus API públicas. Ahora nos centraremos en cómo implementé esta biblioteca.

Implementación de la biblioteca GUI de damas

La biblioteca de damas interfaz gráfica de usuario se compone de cuatro tipos públicos ubicados en los archivos del mismo nombre de código: AlreadyOccupiedException, Board, Checker, y CheckerType. El Listado 1 presenta AlreadyOccupiedExceptionel código fuente.

Listado 1. AlreadyOccupiedException.java

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

AlreadyOccupiedExceptionextiende java.lang.RuntimeException, que hace AlreadyOccupiedExceptionuna excepción sin marcar (no tiene que ser capturada o declarada en una throwscláusula). Si quisiera hacer AlreadyOccupiedExceptioncheck, me habría extendido java.lang.Exception. Elegí desmarcar este tipo porque funciona de manera similar al desmarcado IllegalArgumentException.

AlreadyOccupiedExceptiondeclara un constructor que toma un argumento de cadena que describe el motivo de la excepción. Este argumento se envía a la RuntimeExceptionsuperclase.

Listado 2 presenta Board.

Listado 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 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  8) throw new IllegalArgumentException("row out of range: " + row); if (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; } }

Boardextiende javax.swing.JComponent, lo que crea Boardun componente Swing. Como tal, puede agregar directamente un Boardcomponente al panel de contenido de una aplicación Swing.

Boarddeclara SQUAREDIMy BOARDDIMconstantes que identifican las dimensiones en píxeles de un cuadrado y el tablero. Al inicializar SQUAREDIM, invoco en Checker.getDimension()lugar de acceder a una Checkerconstante pública equivalente . Joshua Block responde por qué hago esto en el ítem # 30 (Use enumeraciones en lugar de intconstantes) de la segunda edición de su libro, Effective Java : "Los programas que usan el intpatrón enum son frágiles. Debido a que las intenumeraciones son constantes en tiempo de compilación, se compilan en los clientes que los usan. Si intse cambia el asociado con una constante de enumeración, sus clientes deben ser recompilados. Si no lo están, aún se ejecutarán, pero su comportamiento no estará definido ".

Debido a los extensos comentarios, no tengo mucho más que decir Board. Sin embargo, tenga en cuenta la PosCheckclase anidada , que describe un verificador posicionado almacenando una Checkerreferencia y sus coordenadas centrales, que son relativas a la esquina superior izquierda del Boardcomponente. Cuando agrega un Checkerobjeto al Board, se almacena en un nuevo PosCheckobjeto junto con la posición central del verificador, que se calcula a partir de la fila y columna especificadas.

El Listado 3 presenta Checker.