Dama, chiunque?

Diversi mesi fa, mi è stato chiesto di creare una piccola libreria Java a cui è possibile accedere da un'applicazione per eseguire il rendering di un'interfaccia utente grafica (GUI) per il gioco di Dama. Oltre a rendere una scacchiera e una pedina, la GUI deve consentire il trascinamento di una scacchiera da un quadrato all'altro. Inoltre, una pedina deve essere centrata su una casella e non deve essere assegnata a una casella occupata da un'altra pedina. In questo post vi presento la mia libreria.

Progettazione di una libreria GUI per dama

Quali tipi di pubblico dovrebbe supportare la biblioteca? Nelle dama, ciascuno dei due giocatori muove alternativamente una delle sue pedine regolari (non re) su una scacchiera solo in avanti e possibilmente salta la pedina dell'altro giocatore. Quando la pedina raggiunge l'altro lato, viene promossa a re, che può anche muoversi all'indietro. Da questa descrizione possiamo dedurre i seguenti tipi:

  • Board
  • Checker
  • CheckerType
  • Player

Un Boardoggetto identifica la scacchiera. Funge da contenitore per Checkeroggetti che occupano varie piazze. Può disegnare se stesso e richiedere che ogni Checkeroggetto contenuto disegni se stesso.

Un Checkeroggetto identifica un controllore. Ha un colore e un'indicazione se si tratta di una pedina normale o di una pedina re. Può disegnare se stesso e mette a disposizione la sua dimensione Board, la cui dimensione è influenzata dalla Checkerdimensione.

CheckerTypeè un enum che identifica un controllo del colore e tipo con i suoi quattro costanti: BLACK_KING, BLACK_REGULAR, RED_KING, e RED_REGULAR.

Un Playeroggetto è un controller per muovere una pedina con salti opzionali. Perché ho scelto di implementare questo gioco in Swing, Playernon è necessario. Invece, mi sono trasformato Boardin un componente Swing il cui costruttore registra mouse e ascoltatori di movimento del mouse che gestiscono il movimento della pedina per conto del giocatore umano. In futuro, potrei implementare un lettore per computer tramite un altro thread, un sincronizzatore e un altro Boardmetodo (come move()).

Quali API pubbliche fanno Boarde Checkercontribuiscono? Dopo un po 'di riflessione, ho creato la seguente BoardAPI pubblica :

  • Board(): Costruisci un Boardoggetto. Il costruttore esegue varie attività di inizializzazione come la registrazione del listener.
  • void add(Checker checker, int row, int column): Aggiungere checkeralla Boardposizione identificata da rowe column. La riga e la colonna sono valori basati su 1 invece di essere basati su 0 (vedere la Figura 1). Il add()genera java.lang.IllegalArgumentExceptionquando il suo argomento di riga o colonna è minore di 1 o maggiore di 8. Inoltre, genera il segno di spunta AlreadyOccupiedExceptionquando si tenta di aggiungere a Checkera un quadrato occupato.
  • Dimension getPreferredSize(): Restituisce la Boarddimensione preferita del componente ai fini del layout.

Figura 1. L'angolo superiore sinistro della scacchiera si trova in (1, 1)

Ho anche sviluppato la seguente CheckerAPI pubblica :

  • Checker(CheckerType checkerType): Costruire un Checkeroggetto della specificato checkerType( BLACK_KING, BLACK_REGULAR, RED_KING, o RED_REGULAR).
  • void draw(Graphics g, int cx, int cy): Disegna a Checkerutilizzando il contesto grafico specificato gcon il centro del checker situato in ( cx, cy). Questo metodo può essere chiamato Boardsolo da .
  • boolean contains(int x, int y, int cx, int cy): Un staticmetodo di supporto chiamato da Boardche determina se le coordinate del mouse ( x, y) si trovano all'interno del checker le cui coordinate centrali sono specificate da ( cx, cy) e la cui dimensione è specificata altrove nella Checkerclasse.
  • int getDimension(): Un staticmetodo di supporto chiamato da Boardche determina la dimensione di una pedina in modo che la scacchiera possa dimensionare adeguatamente i suoi quadrati e la dimensione complessiva.

Questo copre praticamente tutta la libreria della GUI di checkers in termini di tipi e API pubbliche. Ci concentreremo ora su come ho implementato questa libreria.

Implementazione della libreria GUI checkers

La libreria pedine GUI si compone di quattro tipi pubblici situati in file con lo stesso nome di origine: AlreadyOccupiedException, Board, Checker, e CheckerType. Il listato 1 presenta AlreadyOccupiedExceptionil codice sorgente di.

Listato 1. AlreadyOccupiedException.java

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

AlreadyOccupiedExceptionextends java.lang.RuntimeException, che fa AlreadyOccupiedExceptionun'eccezione non controllata (non deve essere catturata o dichiarata in una throwsclausola). Se avessi voluto fare un AlreadyOccupiedExceptioncontrollo, avrei prolungato java.lang.Exception. Ho scelto di rendere questo tipo deselezionato perché funziona in modo simile a quello deselezionato IllegalArgumentException.

AlreadyOccupiedExceptiondichiara un costruttore che accetta un argomento stringa che descrive il motivo dell'eccezione. Questo argomento viene inoltrato alla RuntimeExceptionsuperclasse.

Listato 2 presenta Board.

Listato 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; } }

Boardsi estende javax.swing.JComponent, il che rende Boardun componente Swing. Pertanto, puoi aggiungere direttamente un Boardcomponente al riquadro del contenuto di un'applicazione Swing.

Boarddichiarazioni SQUAREDIMe BOARDDIMcostanti che identificano le dimensioni in pixel di un quadrato e della scacchiera. Durante l'inizializzazione SQUAREDIM, invoco Checker.getDimension()invece di accedere a una Checkercostante pubblica equivalente . Joshua Block risponde perché lo faccio nell'articolo # 30 (Usa enumerazioni invece di intcostanti) della seconda edizione del suo libro, Effective Java : "I programmi che usano il intmodello enum sono fragili. Poiché le intenumerazioni sono costanti in fase di compilazione, vengono compilate nei client che li utilizzano. Se la intcostante associata a una enum viene modificata, i suoi client devono essere ricompilati. Se non lo sono, verranno comunque eseguiti, ma il loro comportamento non sarà definito. "

A causa degli ampi commenti, non ho molto altro da dire Board. Tuttavia, si noti la PosCheckclasse nidificata , che descrive un controllo posizionato memorizzando un Checkerriferimento e le sue coordinate centrali, che sono relative all'angolo superiore sinistro del Boardcomponente. Quando aggiungi un Checkeroggetto a Board, viene memorizzato in un nuovo PosCheckoggetto insieme alla posizione centrale del controllo, che viene calcolata dalla riga e colonna specificate.

Listato 3 presenta Checker.