Ktoś w warcaby?

Kilka miesięcy temu poproszono mnie o stworzenie małej biblioteki Java, do której będzie miała dostęp aplikacja do renderowania graficznego interfejsu użytkownika (GUI) do gry w warcaby. Oprócz renderowania szachownicy i warcabów, GUI musi umożliwiać przeciąganie szachownicy z jednego kwadratu do drugiego. Ponadto pionek musi być wyśrodkowany na kwadracie i nie może być przypisany do kwadratu zajmowanego przez inny pionek. W tym poście przedstawiam swoją bibliotekę.

Projektowanie biblioteki GUI warcaby

Jakie typy publiczne powinna wspierać biblioteka? W warcabach, każdy z dwóch graczy na przemian przesuwa jeden ze swoich regularnych (niekrólowych) pionków po szachownicy tylko w kierunku do przodu i ewentualnie przeskakuje pionki innego gracza. Kiedy pionek dotrze na drugą stronę, awansuje do króla, który może również poruszać się do tyłu. Z tego opisu możemy wywnioskować następujące typy:

  • Board
  • Checker
  • CheckerType
  • Player

BoardObiekt identyfikuje szachownicy. Służy jako pojemnik na Checkerobiekty zajmujące różne kwadraty. Może sam się narysować i zażądać, aby każdy zawarty Checkerobiekt sam się narysował.

CheckerObiekt identyfikuje sprawdzania. Ma kolor i wskazanie, czy jest to zwykły warcaby czy królewski warcaby. Potrafi sam się rysować i udostępnia swój rozmiar Board, na którego wielkość ma wpływ Checkerrozmiar.

CheckerTypejest enum, który identyfikuje kolor i rodzaj sprawdzania poprzez jego czterech stałych: BLACK_KING, BLACK_REGULAR, RED_KING, i RED_REGULAR.

PlayerObiekt jest kontrolerem do przemieszczania sprawdzania z opcjonalnymi skoków. Ponieważ zdecydowałem się zaimplementować tę grę w Swing, Playernie jest to konieczne. Zamiast tego przekształciłem Boardsię w komponent Swing, którego konstruktor rejestruje myszy i słuchacze ruchu myszy, które obsługują ruch szachownicy w imieniu gracza. W przyszłości mógłbym zaimplementować odtwarzacz komputerowy przez inny wątek, synchronizator i inną Boardmetodę (np. move()).

Jakie publiczne interfejsy API robią Boardi Checkerudostępniają? Po namyśle wymyśliłem następujący publiczny Boardinterfejs API:

  • Board(): Skonstruuj Boardobiekt. Konstruktor wykonuje różne zadania inicjalizacyjne, takie jak rejestracja nasłuchiwania.
  • void add(Checker checker, int row, int column): Dodaj checkerdo Boardw pozycji określonej przez rowi column. Wiersz i kolumna są wartościami od 1, a nie od 0 (patrz Rysunek 1). add()Rzuca java.lang.IllegalArgumentExceptiongdy jego wierszu lub kolumnie argument jest mniejszy niż 1 lub większym niż 8. Ponadto, rzuca odznaczone AlreadyOccupiedExceptionpodczas próby dodania Checkerdo zajętego kwadratowy.
  • Dimension getPreferredSize(): Zwróć Boardpreferowany rozmiar komponentu do celów układu.

Rysunek 1. Lewy górny róg szachownicy znajduje się w (1, 1)

Opracowałem również następujące publiczne CheckerAPI:

  • Checker(CheckerType checkerType): Skonstruować Checkerprzedmiot określony checkerType( BLACK_KING, BLACK_REGULAR, RED_KING, i RED_REGULAR).
  • void draw(Graphics g, int cx, int cy): Narysuj Checkerużywając określonego kontekstu graficznego gze środkiem szachownicy znajdującym się w ( cx, cy). Ta metoda ma być wywoływana Boardtylko z .
  • boolean contains(int x, int y, int cx, int cy): staticMetoda pomocnicza wywołana z, Boardktóra określa, czy współrzędne myszy ( x, y) znajdują się wewnątrz szachownicy, której współrzędne środka są określone przez ( cx, cy) i którego wymiar jest określony w innym miejscu w Checkerklasie.
  • int getDimension(): staticMetoda pomocnicza wywoływana z, Boardktóra określa rozmiar pionka, tak aby szachownica mogła odpowiednio dopasować rozmiar swoich kwadratów i całkowity rozmiar.

To prawie obejmuje całą bibliotekę GUI kontrolerów pod względem jej typów i publicznych interfejsów API. Skoncentrujemy się teraz na tym, jak zaimplementowałem tę bibliotekę.

Implementacja biblioteki GUI Checkers

Biblioteka warcaby GUI składa się z czterech rodzajów publicznych znajdujących się w tej samej nazwie plików źródłowych: AlreadyOccupiedException, Board, Checker, i CheckerType. Listing 1 przedstawia AlreadyOccupiedExceptionkod źródłowy.

Listing 1. AlreadyOccupiedException.java

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

AlreadyOccupiedExceptionextends java.lang.RuntimeException, który tworzy AlreadyOccupiedExceptionniezaznaczony wyjątek (nie musi być przechwytywany ani deklarowany w throwsklauzuli). Gdybym chciał się AlreadyOccupiedExceptionsprawdzić, przedłużyłbym java.lang.Exception. Zdecydowałem się odznaczać ten typ, ponieważ działa podobnie do niezaznaczonego IllegalArgumentException.

AlreadyOccupiedExceptiondeklaruje konstruktor, który przyjmuje argument w postaci ciągu opisujący przyczynę wyjątku. Ten argument jest przekazywany do RuntimeExceptionsuperklasy.

Listing 2 przedstawia 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 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; } }

Boardwydłuża się javax.swing.JComponent, Boardtworząc komponent Swing. W związku z tym możesz bezpośrednio dodać Boardkomponent do panelu zawartości aplikacji Swing.

Boarddeklaruje SQUAREDIMi BOARDDIMstałe, które identyfikują wymiary kwadratu i szachownicy w pikselach. Podczas inicjalizacji SQUAREDIMwywołuję Checker.getDimension()zamiast uzyskiwania dostępu do równoważnej publicznej Checkerstałej. Joshua Block odpowiada, dlaczego robię to w punkcie 30 (Użyj wyliczeń zamiast intstałych) drugiej edycji jego książki Efektywna Java : „Programy używające intwzorca wyliczenia są kruche. Ponieważ wyliczenia są intstałymi czasu kompilacji, są one kompilowane do klientów, którzy ich używają. Jeśli zmieniona zostanie intskojarzona ze stałą wyliczeniową, jej klienci muszą zostać ponownie skompilowani. Jeśli tak nie jest, będą nadal działać, ale ich zachowanie będzie niezdefiniowane. "

Ze względu na obszerne komentarze nie mam nic więcej do powiedzenia Board. Zwróć jednak uwagę na PosCheckklasę zagnieżdżoną , która opisuje kontroler pozycjonowany, przechowując Checkerodniesienie i jego współrzędne środkowe, które odnoszą się do lewego górnego rogu Boardkomponentu. Po dodaniu Checkerobiektu do Board, jest on przechowywany w nowym PosCheckobiekcie wraz ze środkową pozycją sprawdzarki, która jest obliczana na podstawie określonego wiersza i kolumny.

Listing 3 przedstawia Checker.