├── .gitignore ├── images ├── sample-game.png └── multiplayer-game.png ├── src ├── network │ ├── CheckersConnection.java │ ├── Session.java │ ├── ConnectionHandler.java │ ├── Command.java │ ├── ConnectionListener.java │ └── CheckersNetworkHandler.java ├── model │ ├── HumanPlayer.java │ ├── NetworkPlayer.java │ ├── Player.java │ ├── Move.java │ ├── Game.java │ ├── ComputerPlayer.java │ └── Board.java ├── ui │ ├── Main.java │ ├── CheckersWindow.java │ ├── OptionPanel.java │ ├── NetworkWindow.java │ └── CheckerBoard.java └── logic │ ├── MoveGenerator.java │ └── MoveLogic.java └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | /bin/ 4 | .classpath 5 | .project 6 | 7 | .DS_Store 8 | Thumbs.db 9 | -------------------------------------------------------------------------------- /images/sample-game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevonMcGrath/Java-Checkers/HEAD/images/sample-game.png -------------------------------------------------------------------------------- /images/multiplayer-game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevonMcGrath/Java-Checkers/HEAD/images/multiplayer-game.png -------------------------------------------------------------------------------- /src/network/CheckersConnection.java: -------------------------------------------------------------------------------- 1 | package network; 2 | 3 | import java.awt.event.ActionEvent; 4 | import java.awt.event.ActionListener; 5 | 6 | public class CheckersConnection implements ActionListener { 7 | 8 | @Override 9 | public void actionPerformed(ActionEvent e) { 10 | // TODO Auto-generated method stub 11 | 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/model/HumanPlayer.java: -------------------------------------------------------------------------------- 1 | /* Name: HumanPlayer 2 | * Author: Devon McGrath 3 | * Description: This class represents a human player (i.e. a user) that can 4 | * interact with the system. 5 | */ 6 | 7 | package model; 8 | 9 | /** 10 | * The {@code HumanPlayer} class represents a user of the checkers game that 11 | * can update the game by clicking on tiles on the board. 12 | */ 13 | public class HumanPlayer extends Player { 14 | 15 | @Override 16 | public boolean isHuman() { 17 | return true; 18 | } 19 | 20 | /** 21 | * Performs no updates on the game. As human players can interact with the 22 | * user interface to update the game. 23 | */ 24 | @Override 25 | public void updateGame(Game game) {} 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/model/NetworkPlayer.java: -------------------------------------------------------------------------------- 1 | /* Name: NetworkPlayer 2 | * Author: Devon McGrath 3 | * Description: This class represents a network player, who may be on a 4 | * different host. 5 | */ 6 | 7 | package model; 8 | 9 | /** 10 | * The {@code NetworkPlayer} class is a dummy player used so that the game 11 | * can be updated properly from the corresponding client. 12 | */ 13 | public class NetworkPlayer extends Player { 14 | 15 | @Override 16 | public boolean isHuman() { 17 | return false; 18 | } 19 | 20 | /** 21 | * This method does not actually update the game state for network players 22 | * as it is updated when their client sends the updated game. 23 | */ 24 | @Override 25 | public void updateGame(Game game) {} 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/model/Player.java: -------------------------------------------------------------------------------- 1 | /* Name: Player 2 | * Author: Devon McGrath 3 | * Description: This class represents a player of the system. 4 | */ 5 | 6 | package model; 7 | 8 | /** 9 | * The {@code Player} class is an abstract class that represents a player in a 10 | * game of checkers. 11 | */ 12 | public abstract class Player { 13 | 14 | /** 15 | * Determines how the game is updated. If true, the user must interact with 16 | * the user interface to make a move. Otherwise, the game is updated via 17 | * {@link #updateGame(Game)}. 18 | * 19 | * @return true if this player represents a user. 20 | */ 21 | public abstract boolean isHuman(); 22 | 23 | /** 24 | * Updates the game state to take a move for the current player. If there 25 | * is a move available that is multiple skips, it may be performed at once 26 | * by this method or one skip at a time. 27 | * 28 | * @param game the game to update. 29 | */ 30 | public abstract void updateGame(Game game); 31 | 32 | @Override 33 | public String toString() { 34 | return getClass().getSimpleName() + "[isHuman=" + isHuman() + "]"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ui/Main.java: -------------------------------------------------------------------------------- 1 | /* Name: Main 2 | * Author: Devon McGrath 3 | * Description: This class contains the main method to create the GUI and 4 | * start the checkers game. 5 | * ============================================================================ 6 | * JAVA CHECKERS 7 | * ------------- 8 | * Author: Devon McGrath (https://github.com/DevonMcGrath) 9 | * Created: 2018-04-14 10 | * Description: This program is a simple implementation of the standard 11 | * checkers game, with standard rules, in Java. 12 | * 13 | * RULES 14 | * >>>>>>>>>>>>> 15 | * 1. Checkers can only move diagonally, on dark tiles. 16 | * 17 | * 2. Normal checkers can only move forward diagonally (for black checkers, 18 | * this is down and for white checkers, this is up). 19 | * 20 | * 3. A checker becomes a king when it reaches the opponents end and cannot 21 | * move forward anymore. 22 | * 23 | * 4. Once a checker becomes a king, the player's turn is over. 24 | * 25 | * 5. After a checker/king moves one space diagonally, the player's turn is 26 | * over. 27 | * 28 | * 6. If an opponent's checker/king can be skipped, it must be skipped. 29 | * 30 | * 7. If after a skip, the same checker can skip again, it must. Otherwise, 31 | * the turn is over. 32 | * 33 | * 8. The game is over if a player either has no more checkers or cannot make 34 | * a move on their turn. 35 | * 36 | * 9. The player with the black checkers moves first. 37 | */ 38 | 39 | package ui; 40 | 41 | import javax.swing.UIManager; 42 | 43 | public class Main { 44 | 45 | public static void main(String[] args) { 46 | 47 | // Set the look and feel to the OS look and feel 48 | try { 49 | UIManager.setLookAndFeel( 50 | UIManager.getSystemLookAndFeelClassName()); 51 | } catch (Exception e) { 52 | e.printStackTrace(); 53 | } 54 | 55 | // Create a window to display the checkers game 56 | CheckersWindow window = new CheckersWindow(); 57 | window.setDefaultCloseOperation(CheckersWindow.EXIT_ON_CLOSE); 58 | window.setVisible(true); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/model/Move.java: -------------------------------------------------------------------------------- 1 | /* Name: Move 2 | * Author: Devon McGrath 3 | * Description: This class represents a move. 4 | */ 5 | 6 | package model; 7 | 8 | import java.awt.Point; 9 | 10 | /** 11 | * The {@code Move} class represents a move and contains a weight associated 12 | * with the move. 13 | */ 14 | public class Move { 15 | 16 | /** The weight corresponding to an invalid move. */ 17 | public static final double WEIGHT_INVALID = Double.NEGATIVE_INFINITY; 18 | 19 | /** The start index of the move. */ 20 | private byte startIndex; 21 | 22 | /** The end index of the move. */ 23 | private byte endIndex; 24 | 25 | /** The weight associated with the move. */ 26 | private double weight; 27 | 28 | public Move(int startIndex, int endIndex) { 29 | setStartIndex(startIndex); 30 | setEndIndex(endIndex); 31 | } 32 | 33 | public Move(Point start, Point end) { 34 | setStartIndex(Board.toIndex(start)); 35 | setEndIndex(Board.toIndex(end)); 36 | } 37 | 38 | public int getStartIndex() { 39 | return startIndex; 40 | } 41 | 42 | public void setStartIndex(int startIndex) { 43 | this.startIndex = (byte) startIndex; 44 | } 45 | 46 | public int getEndIndex() { 47 | return endIndex; 48 | } 49 | 50 | public void setEndIndex(int endIndex) { 51 | this.endIndex = (byte) endIndex; 52 | } 53 | 54 | public Point getStart() { 55 | return Board.toPoint(startIndex); 56 | } 57 | 58 | public void setStart(Point start) { 59 | setStartIndex(Board.toIndex(start)); 60 | } 61 | 62 | public Point getEnd() { 63 | return Board.toPoint(endIndex); 64 | } 65 | 66 | public void setEnd(Point end) { 67 | setEndIndex(Board.toIndex(end)); 68 | } 69 | 70 | public double getWeight() { 71 | return weight; 72 | } 73 | 74 | public void setWeight(double weight) { 75 | this.weight = weight; 76 | } 77 | 78 | public void changeWeight(double delta) { 79 | this.weight += delta; 80 | } 81 | 82 | @Override 83 | public String toString() { 84 | return getClass().getSimpleName() + "[startIndex=" + startIndex + ", " 85 | + "endIndex=" + endIndex + ", weight=" + weight + "]"; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/network/Session.java: -------------------------------------------------------------------------------- 1 | /* Name: Session 2 | * Author: Devon McGrath 3 | * Description: This class represents a session between two clients. 4 | */ 5 | 6 | package network; 7 | 8 | /** 9 | * The {@code Session} class represents a session between this client and a 10 | * remote checkers client. It contains the important connection information 11 | * that is used to pass messages and accept messages. 12 | */ 13 | public class Session { 14 | 15 | /** The listener handling the connection on this client. */ 16 | private ConnectionListener listener; 17 | 18 | /** The session ID used for correspondence between the two clients. */ 19 | private String sid; 20 | 21 | /** The destination host name or IP. */ 22 | private String destinationHost; 23 | 24 | /** The destination port. */ 25 | private int destinationPort; 26 | 27 | public Session(ConnectionListener listener, String sid, 28 | String destinationHost, int destinationPort) { 29 | this.listener = listener; 30 | this.sid = sid; 31 | this.destinationHost = destinationHost; 32 | this.destinationPort = destinationPort; 33 | } 34 | 35 | public Session(String sid, int sourcePort, 36 | String destinationHost, int destinationPort) { 37 | this.listener = new ConnectionListener(sourcePort); 38 | this.sid = sid; 39 | this.destinationHost = destinationHost; 40 | this.destinationPort = destinationPort; 41 | } 42 | 43 | public ConnectionListener getListener() { 44 | return listener; 45 | } 46 | 47 | public void setListener(ConnectionListener listener) { 48 | this.listener = listener; 49 | } 50 | 51 | public String getSid() { 52 | return sid; 53 | } 54 | 55 | public void setSid(String sid) { 56 | this.sid = sid; 57 | } 58 | 59 | public String getDestinationHost() { 60 | return destinationHost; 61 | } 62 | 63 | public void setDestinationHost(String destinationHost) { 64 | this.destinationHost = destinationHost; 65 | } 66 | 67 | public int getDestinationPort() { 68 | return destinationPort; 69 | } 70 | 71 | public void setDestinationPort(int destinationPort) { 72 | this.destinationPort = destinationPort; 73 | } 74 | 75 | public int getSourcePort() { 76 | return (listener == null? -1 : listener.getPort()); 77 | } 78 | 79 | public void setSourcePort(int sourcePort) { 80 | if (listener != null) { 81 | this.listener.setPort(sourcePort); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/network/ConnectionHandler.java: -------------------------------------------------------------------------------- 1 | /* Name: ConnectionHandler 2 | * Author: Devon McGrath 3 | * Description: This class is responsible for handling a connection to the 4 | * ConnectionListener. 5 | */ 6 | 7 | package network; 8 | 9 | import java.awt.event.ActionEvent; 10 | import java.net.Socket; 11 | 12 | /** 13 | * The {@code ConnectionHandler} class handles a connection to an instance of 14 | * the {@link ConnectionListener} class. Once created, it will be run on a new 15 | * thread immediately after the connection is made and invokes the action 16 | * listener from the {@code ConnectionListener} class (if one is specified). 17 | */ 18 | public class ConnectionHandler extends Thread { 19 | 20 | /** The connection listener that created this handler. */ 21 | private ConnectionListener listener; 22 | 23 | /** The connection from the remote client to this one. */ 24 | private Socket socket; 25 | 26 | /** 27 | * Creates a connection handler that is capable of handling an incoming 28 | * connection. 29 | * 30 | * @param listener the listener to which the connection was made. 31 | * @param socket the actual connection between the two processes. 32 | */ 33 | public ConnectionHandler(ConnectionListener listener, Socket socket) { 34 | this.listener = listener; 35 | this.socket = socket; 36 | } 37 | 38 | /** 39 | * Runs the action listener handler from the {@link ConnectionListener} 40 | * instance that the connection was made to. If the action listener was not 41 | * specified, then this method does nothing. 42 | *

43 | * Note: this method should be called using {@link #start()} and not called 44 | * directly to allow it to run on a new thread. 45 | */ 46 | @Override 47 | public void run() { 48 | 49 | if (listener == null) { 50 | return; 51 | } 52 | 53 | // Send the event to the handler 54 | ActionEvent e = new ActionEvent(this, 0, "CONNECTION ACCEPT"); 55 | if (listener.getConnectionHandler() != null) { 56 | this.listener.getConnectionHandler().actionPerformed(e); 57 | } 58 | } 59 | 60 | /** 61 | * Gets the listener associated with the connection. 62 | * 63 | * @return the connection listener. 64 | */ 65 | public ConnectionListener getListener() { 66 | return listener; 67 | } 68 | 69 | /** 70 | * Gets the socket connection between this process and the remote host's 71 | * process. 72 | * 73 | * @return the connection. 74 | */ 75 | public Socket getSocket() { 76 | return socket; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java Checkers 2 | Sample Checkers Game 3 | 4 | ## Description 5 | A checkers game with a GUI implemented in Java. The program supports a simple computer player as well as another checkers program instance connected through the network. 6 | 7 | The computer player works by assigning a weight for each move. When a move weight is calculated, it is based off information such as move safety (e.g. if the move results in the checker being taken by the other player, it is less likely to make that move). It checks a number of other factors and assigns a final weight. The move with the highest weight is chosen as the move. To ensure computer players are less predictable, if multiple moves have the same weight then one is randomly chosen. 8 | 9 | ## Compile and Run 10 | ### Manual 11 | 1. In terminal/command prompt, navigate to `src/` 12 | 1. Compile with `javac ui/*.java model/*.java logic/*.java network/*.java` 13 | 1. Run with `java ui.Main` 14 | 15 | ## Features 16 | ### User Interface 17 | The checkers program comes complete with all graphical user interface components that scale relative to the size of the window. It is a user-friendly UI and has options to change the type of player for both player 1 and 2, and restart the game. In addition, it provides a checker board UI to show the current game state. 18 | 19 | ### Different Player Types 20 | Multiple different types of players are supported and can be selected by the user: 21 | 1. Human - this is the player that allows the user to interact with the checker board when it is their turn. 22 | 1. Computer - implements simple logic to make smart moves, without input from the user. 23 | 1. Network - this type of player represents a player on a remote checkers client which can make moves. 24 | 25 | All player classes extend the abstract `Player` class and either implement the logic to update the game or allow the user to input their moves. 26 | 27 | ### Peer to Peer Connections 28 | ![Network Game Setup Example](images/multiplayer-game.png) 29 | 30 | Each instance of a checkers window/program is network capable. It is able to make connections across the network to other checkers clients. To set up a network player, simply do the following: 31 | 1. Select "Network" as the type of player, for the player you want to be controlled by a remote client. 32 | 1. Click the "Set Connection" button 33 | 1. Enter the source port that this client will listen for connections on (1025 to 65535). 34 | 1. Click "Listen". 35 | 1. Repeat steps 1 - 4 on the second client but for the other player (e.g. if client 1 has player 2 as a network player, then client 2 needs player 1 as a network player). 36 | 1. Enter the host name or IP in the destination host field (e.g. 127.0.0.1, localhost, etc). 37 | 1. Enter the destination port that the other client is listening on. 38 | 1. Click "Connect". 39 | 40 | In addition to peer-to-peer connections, a checkers window can act as a router and forward the game state between two clients if both of the players are network players. 41 | 42 | ### Network Security 43 | To prevent a third checkers client from interfering with the game state for peer-to-peer connections, some level of network security has been added. When a remote client makes a new connection, it receives a randomly generated session ID that must be used in all following messages between the clients. It's important to note that while the inclusion of session IDs prevents other checker clients from interfering with a peer-to-peer game, the messages sent between clients are not encrypted and can therefore be sniffed and/or modified on the network. 44 | -------------------------------------------------------------------------------- /src/ui/CheckersWindow.java: -------------------------------------------------------------------------------- 1 | /* Name: CheckersWindow 2 | * Author: Devon McGrath 3 | * Description: This class is a window that is used to play a game of checkers. 4 | * It also contains a component to change the game options. 5 | */ 6 | 7 | package ui; 8 | 9 | import java.awt.BorderLayout; 10 | 11 | import javax.swing.JFrame; 12 | import javax.swing.JPanel; 13 | 14 | import model.Player; 15 | import network.CheckersNetworkHandler; 16 | import network.ConnectionListener; 17 | import network.Session; 18 | 19 | /** 20 | * The {@code CheckersWindow} class is responsible for managing a window. This 21 | * window contains a game of checkers and also options to change the settings 22 | * of the game with an {@link OptionPanel}. 23 | */ 24 | public class CheckersWindow extends JFrame { 25 | 26 | private static final long serialVersionUID = 8782122389400590079L; 27 | 28 | /** The default width for the checkers window. */ 29 | public static final int DEFAULT_WIDTH = 500; 30 | 31 | /** The default height for the checkers window. */ 32 | public static final int DEFAULT_HEIGHT = 600; 33 | 34 | /** The default title for the checkers window. */ 35 | public static final String DEFAULT_TITLE = "Java Checkers"; 36 | 37 | /** The checker board component playing the updatable game. */ 38 | private CheckerBoard board; 39 | 40 | private OptionPanel opts; 41 | 42 | private Session session1; 43 | 44 | private Session session2; 45 | 46 | public CheckersWindow() { 47 | this(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_TITLE); 48 | } 49 | 50 | public CheckersWindow(Player player1, Player player2) { 51 | this(); 52 | setPlayer1(player1); 53 | setPlayer2(player2); 54 | } 55 | 56 | public CheckersWindow(int width, int height, String title) { 57 | 58 | // Setup the window 59 | super(title); 60 | super.setSize(width, height); 61 | super.setLocationByPlatform(true); 62 | 63 | // Setup the components 64 | JPanel layout = new JPanel(new BorderLayout()); 65 | this.board = new CheckerBoard(this); 66 | this.opts = new OptionPanel(this); 67 | layout.add(board, BorderLayout.CENTER); 68 | layout.add(opts, BorderLayout.SOUTH); 69 | this.add(layout); 70 | 71 | // Setup the network listeners 72 | CheckersNetworkHandler session1Handler, session2Handler; 73 | session1Handler = new CheckersNetworkHandler(true, this, board, opts); 74 | session2Handler = new CheckersNetworkHandler(false, this, board, opts); 75 | this.session1 = new Session(new ConnectionListener( 76 | 0, session1Handler), null, null, -1); 77 | this.session2 = new Session(new ConnectionListener( 78 | 0, session2Handler), null, null, -1); 79 | } 80 | 81 | public CheckerBoard getBoard() { 82 | return board; 83 | } 84 | 85 | /** 86 | * Updates the type of player that is being used for player 1. 87 | * 88 | * @param player1 the new player instance to control player 1. 89 | */ 90 | public void setPlayer1(Player player1) { 91 | this.board.setPlayer1(player1); 92 | this.board.update(); 93 | } 94 | 95 | /** 96 | * Updates the type of player that is being used for player 2. 97 | * 98 | * @param player2 the new player instance to control player 2. 99 | */ 100 | public void setPlayer2(Player player2) { 101 | this.board.setPlayer2(player2); 102 | this.board.update(); 103 | } 104 | 105 | /** 106 | * Resets the game of checkers in the window. 107 | */ 108 | public void restart() { 109 | this.board.getGame().restart(); 110 | this.board.update(); 111 | } 112 | 113 | public void setGameState(String state) { 114 | this.board.getGame().setGameState(state); 115 | } 116 | 117 | public Session getSession1() { 118 | return session1; 119 | } 120 | 121 | public Session getSession2() { 122 | return session2; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/network/Command.java: -------------------------------------------------------------------------------- 1 | /* Name: Command 2 | * Author: Devon McGrath 3 | * Description: This class represents a command that can be sent to another 4 | * checkers window. 5 | */ 6 | 7 | package network; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.io.PrintWriter; 13 | import java.net.Socket; 14 | import java.net.UnknownHostException; 15 | 16 | /** 17 | * The {@code Command} class is used to represent a command to send from one 18 | * checkers client to another. It defines the standard commands to send. 19 | */ 20 | public class Command { 21 | 22 | /** The command to send the current game state to the connected client. 23 | * Note: a matching SID is required for the game to be updated. */ 24 | public static final String COMMAND_UPDATE = "UPDATE"; 25 | 26 | /** The command to try to connect to another checkers client. Note: this 27 | * command requires two additional lines: 1) the remote port that should be 28 | * connected to, and 2) either "1" or "2" indicating if the remote client 29 | * is connecting as player 1 or 2. */ 30 | public static final String COMMAND_CONNECT = "CONNECT"; 31 | 32 | /** The command to disconnect from the remote client (e.g. when the client 33 | * stops the program). Note: this command requires one additional line of 34 | * the matching SID. */ 35 | public static final String COMMAND_DISCONNECT = "DISCONNECT"; 36 | 37 | /** The command to get the game state from a remote client. Note: a 38 | * matching SID is required for the game state to be sent. */ 39 | public static final String COMMAND_GET = "GET-STATE"; 40 | 41 | /** The command to issue. */ 42 | private String command; 43 | 44 | /** The data on the following lines. */ 45 | private String[] data; 46 | 47 | /** 48 | * Constructs a command with the data. 49 | * 50 | * @param command the command to send. 51 | * @param data the data to send (where each element is a line). 52 | */ 53 | public Command(String command, String... data) { 54 | this.command = command; 55 | this.data = data; 56 | } 57 | 58 | /** 59 | * Sends the command and the data to the specified host and port. It then 60 | * reads and returns the response from the other host. 61 | * 62 | * @param host the remote host (e.g. 127.0.0.1). 63 | * @param port the port to connect to. 64 | * @return the response from the host or an empty string if an error 65 | * occurred. 66 | * @see {@link #getOutput()} 67 | */ 68 | public String send(String host, int port) { 69 | 70 | String data = getOutput(), response = ""; 71 | try { 72 | 73 | // Write the response 74 | Socket s = new Socket(host, port); 75 | PrintWriter writer = new PrintWriter(s.getOutputStream()); 76 | writer.println(data); 77 | writer.flush(); 78 | 79 | // Get the response 80 | BufferedReader br = new BufferedReader(new InputStreamReader( 81 | s.getInputStream())); 82 | String line = null; 83 | while ((line = br.readLine()) != null) { 84 | response += line + "\n"; 85 | } 86 | if (!response.isEmpty()) { 87 | response = response.substring(0, response.length() - 1); 88 | } 89 | s.close(); 90 | 91 | } catch (UnknownHostException e) { 92 | e.printStackTrace(); 93 | } catch (IOException e) { 94 | e.printStackTrace(); 95 | } 96 | 97 | return response; 98 | } 99 | 100 | /** 101 | * Gets the output that will be sent for this command and is the 102 | * combination of the command as the first line and each line in the 103 | * data up until the first null value (or the end of the data). 104 | * 105 | * @return the output from this command. 106 | */ 107 | public String getOutput() { 108 | 109 | String out = command; 110 | 111 | // Add the lines until the first null value 112 | int n = data == null? 0 : data.length; 113 | for (int i = 0; i < n; i ++) { 114 | if (data[i] == null) { 115 | break; 116 | } 117 | out += "\n" + data[i]; 118 | } 119 | 120 | return out; 121 | } 122 | 123 | public String getCommand() { 124 | return command; 125 | } 126 | 127 | public void setCommand(String command) { 128 | this.command = command; 129 | } 130 | 131 | public String[] getData() { 132 | return data; 133 | } 134 | 135 | public void setData(String[] data) { 136 | this.data = data; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/logic/MoveGenerator.java: -------------------------------------------------------------------------------- 1 | /* Name: MoveGenerator 2 | * Author: Devon McGrath 3 | * Description: This class is responsible for getting possible moves. 4 | */ 5 | 6 | package logic; 7 | 8 | import java.awt.Point; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import model.Board; 13 | 14 | /** 15 | * The {@code MoveGenerator} class provides a method for determining if a given 16 | * checker can make any move or skip. 17 | */ 18 | public class MoveGenerator { 19 | 20 | /** 21 | * Gets a list of move end-points for a given start index. 22 | * 23 | * @param board the board to look for available moves. 24 | * @param start the center index to look for moves around. 25 | * @return the list of points such that the start to a given point 26 | * represents a move available. 27 | * @see {@link #getMoves(Board, int)} 28 | */ 29 | public static List getMoves(Board board, Point start) { 30 | return getMoves(board, Board.toIndex(start)); 31 | } 32 | 33 | /** 34 | * Gets a list of move end-points for a given start index. 35 | * 36 | * @param board the board to look for available moves. 37 | * @param startIndex the center index to look for moves around. 38 | * @return the list of points such that the start to a given point 39 | * represents a move available. 40 | * @see {@link #getMoves(Board, Point)} 41 | */ 42 | public static List getMoves(Board board, int startIndex) { 43 | 44 | // Trivial cases 45 | List endPoints = new ArrayList<>(); 46 | if (board == null || !Board.isValidIndex(startIndex)) { 47 | return endPoints; 48 | } 49 | 50 | // Determine possible points 51 | int id = board.get(startIndex); 52 | Point p = Board.toPoint(startIndex); 53 | addPoints(endPoints, p, id, 1); 54 | 55 | // Remove invalid points 56 | for (int i = 0; i < endPoints.size(); i ++) { 57 | Point end = endPoints.get(i); 58 | if (board.get(end.x, end.y) != Board.EMPTY) { 59 | endPoints.remove(i --); 60 | } 61 | } 62 | 63 | return endPoints; 64 | } 65 | 66 | /** 67 | * Gets a list of skip end-points for a given starting point. 68 | * 69 | * @param board the board to look for available skips. 70 | * @param start the center index to look for skips around. 71 | * @return the list of points such that the start to a given point 72 | * represents a skip available. 73 | * @see {@link #getSkips(Board, int)} 74 | */ 75 | public static List getSkips(Board board, Point start) { 76 | return getSkips(board, Board.toIndex(start)); 77 | } 78 | 79 | /** 80 | * Gets a list of skip end-points for a given start index. 81 | * 82 | * @param board the board to look for available skips. 83 | * @param startIndex the center index to look for skips around. 84 | * @return the list of points such that the start to a given point 85 | * represents a skip available. 86 | * @see {@link #getSkips(Board, Point)} 87 | */ 88 | public static List getSkips(Board board, int startIndex) { 89 | 90 | // Trivial cases 91 | List endPoints = new ArrayList<>(); 92 | if (board == null || !Board.isValidIndex(startIndex)) { 93 | return endPoints; 94 | } 95 | 96 | // Determine possible points 97 | int id = board.get(startIndex); 98 | Point p = Board.toPoint(startIndex); 99 | addPoints(endPoints, p, id, 2); 100 | 101 | // Remove invalid points 102 | for (int i = 0; i < endPoints.size(); i ++) { 103 | 104 | // Check that the skip is valid 105 | Point end = endPoints.get(i); 106 | if (!isValidSkip(board, startIndex, Board.toIndex(end))) { 107 | endPoints.remove(i --); 108 | } 109 | } 110 | 111 | return endPoints; 112 | } 113 | 114 | /** 115 | * Checks if a skip is valid. 116 | * 117 | * @param board the board to check against. 118 | * @param startIndex the start index of the skip. 119 | * @param endIndex the end index of the skip. 120 | * @return true if and only if the skip can be performed. 121 | */ 122 | public static boolean isValidSkip(Board board, 123 | int startIndex, int endIndex) { 124 | 125 | if (board == null) { 126 | return false; 127 | } 128 | 129 | // Check that end is empty 130 | if (board.get(endIndex) != Board.EMPTY) { 131 | return false; 132 | } 133 | 134 | // Check that middle is enemy 135 | int id = board.get(startIndex); 136 | int midID = board.get(Board.toIndex(Board.middle(startIndex, endIndex))); 137 | if (id == Board.INVALID || id == Board.EMPTY) { 138 | return false; 139 | } else if (midID == Board.INVALID || midID == Board.EMPTY) { 140 | return false; 141 | } else if (Board.isBlackChecker(midID) ^ Board.isWhiteChecker(id)) { 142 | return false; 143 | } 144 | 145 | return true; 146 | } 147 | 148 | /** 149 | * Adds points that could potentially result in moves/skips. 150 | * 151 | * @param points the list of points to add to. 152 | * @param p the center point. 153 | * @param id the ID at the center point. 154 | * @param delta the amount to add/subtract. 155 | */ 156 | public static void addPoints(List points, Point p, int id, int delta) { 157 | 158 | // Add points moving down 159 | boolean isKing = Board.isKingChecker(id); 160 | if (isKing || id == Board.BLACK_CHECKER) { 161 | points.add(new Point(p.x + delta, p.y + delta)); 162 | points.add(new Point(p.x - delta, p.y + delta)); 163 | } 164 | 165 | // Add points moving up 166 | if (isKing || id == Board.WHITE_CHECKER) { 167 | points.add(new Point(p.x + delta, p.y - delta)); 168 | points.add(new Point(p.x - delta, p.y - delta)); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/model/Game.java: -------------------------------------------------------------------------------- 1 | /* Name: Game 2 | * Author: Devon McGrath 3 | * Description: This class represents a game of checkers. It provides a method 4 | * to update the game state and keep track of who's turn it is. 5 | */ 6 | 7 | package model; 8 | 9 | import java.awt.Point; 10 | import java.util.List; 11 | 12 | import logic.MoveGenerator; 13 | import logic.MoveLogic; 14 | 15 | /** 16 | * The {@code Game} class represents a game of checkers and ensures that all 17 | * moves made are valid as per the rules of checkers. 18 | */ 19 | public class Game { 20 | 21 | /** The current state of the checker board. */ 22 | private Board board; 23 | 24 | /** The flag indicating if it is player 1's turn. */ 25 | private boolean isP1Turn; 26 | 27 | /** The index of the last skip, to allow for multiple skips in a turn. */ 28 | private int skipIndex; 29 | 30 | public Game() { 31 | restart(); 32 | } 33 | 34 | public Game(String state) { 35 | setGameState(state); 36 | } 37 | 38 | public Game(Board board, boolean isP1Turn, int skipIndex) { 39 | this.board = (board == null)? new Board() : board; 40 | this.isP1Turn = isP1Turn; 41 | this.skipIndex = skipIndex; 42 | } 43 | 44 | /** 45 | * Creates a copy of this game such that any modifications made to one are 46 | * not made to the other. 47 | * 48 | * @return an exact copy of this game. 49 | */ 50 | public Game copy() { 51 | Game g = new Game(); 52 | g.board = board.copy(); 53 | g.isP1Turn = isP1Turn; 54 | g.skipIndex = skipIndex; 55 | return g; 56 | } 57 | 58 | /** 59 | * Resets the game of checkers to the initial state. 60 | */ 61 | public void restart() { 62 | this.board = new Board(); 63 | this.isP1Turn = true; 64 | this.skipIndex = -1; 65 | } 66 | 67 | /** 68 | * Attempts to make a move from the start point to the end point. 69 | * 70 | * @param start the start point for the move. 71 | * @param end the end point for the move. 72 | * @return true if and only if an update was made to the game state. 73 | * @see {@link #move(int, int)} 74 | */ 75 | public boolean move(Point start, Point end) { 76 | if (start == null || end == null) { 77 | return false; 78 | } 79 | return move(Board.toIndex(start), Board.toIndex(end)); 80 | } 81 | 82 | /** 83 | * Attempts to make a move given the start and end index of the move. 84 | * 85 | * @param startIndex the start index of the move. 86 | * @param endIndex the end index of the move. 87 | * @return true if and only if an update was made to the game state. 88 | * @see {@link #move(Point, Point)} 89 | */ 90 | public boolean move(int startIndex, int endIndex) { 91 | 92 | // Validate the move 93 | if (!MoveLogic.isValidMove(this, startIndex, endIndex)) { 94 | return false; 95 | } 96 | 97 | // Make the move 98 | Point middle = Board.middle(startIndex, endIndex); 99 | int midIndex = Board.toIndex(middle); 100 | this.board.set(endIndex, board.get(startIndex)); 101 | this.board.set(midIndex, Board.EMPTY); 102 | this.board.set(startIndex, Board.EMPTY); 103 | 104 | // Make the checker a king if necessary 105 | Point end = Board.toPoint(endIndex); 106 | int id = board.get(endIndex); 107 | boolean switchTurn = false; 108 | if (end.y == 0 && id == Board.WHITE_CHECKER) { 109 | this.board.set(endIndex, Board.WHITE_KING); 110 | switchTurn = true; 111 | } else if (end.y == 7 && id == Board.BLACK_CHECKER) { 112 | this.board.set(endIndex, Board.BLACK_KING); 113 | switchTurn = true; 114 | } 115 | 116 | // Check if the turn should switch (i.e. no more skips) 117 | boolean midValid = Board.isValidIndex(midIndex); 118 | if (midValid) { 119 | this.skipIndex = endIndex; 120 | } 121 | if (!midValid || MoveGenerator.getSkips( 122 | board.copy(), endIndex).isEmpty()) { 123 | switchTurn = true; 124 | } 125 | if (switchTurn) { 126 | this.isP1Turn = !isP1Turn; 127 | this.skipIndex = -1; 128 | } 129 | 130 | return true; 131 | } 132 | 133 | /** 134 | * Gets a copy of the current board state. 135 | * 136 | * @return a non-reference to the current game board state. 137 | */ 138 | public Board getBoard() { 139 | return board.copy(); 140 | } 141 | 142 | /** 143 | * Determines if the game is over. The game is over if one or both players 144 | * cannot make a single move during their turn. 145 | * 146 | * @return true if the game is over. 147 | */ 148 | public boolean isGameOver() { 149 | 150 | // Ensure there is at least one of each checker 151 | List black = board.find(Board.BLACK_CHECKER); 152 | black.addAll(board.find(Board.BLACK_KING)); 153 | if (black.isEmpty()) { 154 | return true; 155 | } 156 | List white = board.find(Board.WHITE_CHECKER); 157 | white.addAll(board.find(Board.WHITE_KING)); 158 | if (white.isEmpty()) { 159 | return true; 160 | } 161 | 162 | // Check that the current player can move 163 | List test = isP1Turn? black : white; 164 | for (Point p : test) { 165 | int i = Board.toIndex(p); 166 | if (!MoveGenerator.getMoves(board, i).isEmpty() || 167 | !MoveGenerator.getSkips(board, i).isEmpty()) { 168 | return false; 169 | } 170 | } 171 | 172 | // No moves 173 | return true; 174 | } 175 | 176 | public boolean isP1Turn() { 177 | return isP1Turn; 178 | } 179 | 180 | public void setP1Turn(boolean isP1Turn) { 181 | this.isP1Turn = isP1Turn; 182 | } 183 | 184 | public int getSkipIndex() { 185 | return skipIndex; 186 | } 187 | 188 | /** 189 | * Gets the current game state as a string of data that can be parsed by 190 | * {@link #setGameState(String)}. 191 | * 192 | * @return a string representing the current game state. 193 | * @see {@link #setGameState(String)} 194 | */ 195 | public String getGameState() { 196 | 197 | // Add the game board 198 | String state = ""; 199 | for (int i = 0; i < 32; i ++) { 200 | state += "" + board.get(i); 201 | } 202 | 203 | // Add the other info 204 | state += (isP1Turn? "1" : "0"); 205 | state += skipIndex; 206 | 207 | return state; 208 | } 209 | 210 | /** 211 | * Parses a string representing a game state that was generated from 212 | * {@link #getGameState()}. 213 | * 214 | * @param state the game state. 215 | * @see {@link #getGameState()} 216 | */ 217 | public void setGameState(String state) { 218 | 219 | restart(); 220 | 221 | // Trivial cases 222 | if (state == null || state.isEmpty()) { 223 | return; 224 | } 225 | 226 | // Update the board 227 | int n = state.length(); 228 | for (int i = 0; i < 32 && i < n; i ++) { 229 | try { 230 | int id = Integer.parseInt("" + state.charAt(i)); 231 | this.board.set(i, id); 232 | } catch (NumberFormatException e) {} 233 | } 234 | 235 | // Update the other info 236 | if (n > 32) { 237 | this.isP1Turn = (state.charAt(32) == '1'); 238 | } 239 | if (n > 33) { 240 | try { 241 | this.skipIndex = Integer.parseInt(state.substring(33)); 242 | } catch (NumberFormatException e) { 243 | this.skipIndex = -1; 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/network/ConnectionListener.java: -------------------------------------------------------------------------------- 1 | /* Name: ConnectionListener 2 | * Author: Devon McGrath 3 | * Description: This class acts as a server and listens for connections on the 4 | * specified port. 5 | */ 6 | 7 | package network; 8 | 9 | import java.awt.event.ActionListener; 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.InputStreamReader; 14 | import java.net.DatagramSocket; 15 | import java.net.ServerSocket; 16 | import java.net.Socket; 17 | 18 | /** 19 | * The {@code ConnectionListener} class listens for connections on a specific 20 | * port. Once a connection is made, it will create an instance of 21 | * {@link ConnectionHandler} and run it on a new thread to handle the 22 | * connection. It will use the connection handler passed either through 23 | * {@link #ConnectionListener(int, ActionListener)} or 24 | * {@link #setConnectionHandler(ActionListener)}. 25 | *

26 | * The action listener will be invoked with a {@code ConnectionHandler} object 27 | * that contains the listener that created it and the socket connection. 28 | */ 29 | public class ConnectionListener extends Thread { 30 | 31 | /** The socket that will listen for connections. */ 32 | private ServerSocket serverSocket; 33 | 34 | /** The action listener that will be invoked when a connection is made. */ 35 | private ActionListener connectionHandler; 36 | 37 | /** 38 | * Creates a connection listener on a dynamically allocated port. 39 | */ 40 | public ConnectionListener() { 41 | this(0); 42 | } 43 | 44 | /** 45 | * Creates a connection listener for the specified port. 46 | * 47 | * @param port the port to listen on. 48 | */ 49 | public ConnectionListener(int port) { 50 | setPort(port); 51 | } 52 | 53 | /** 54 | * Creates a connection listener on the specified port with a connection 55 | * handler. 56 | * 57 | * @param port the port to listen on. 58 | * @param connectionHandler the action listener to handle connections. 59 | */ 60 | public ConnectionListener(int port, ActionListener connectionHandler) { 61 | setPort(port); 62 | this.connectionHandler = connectionHandler; 63 | } 64 | 65 | /** 66 | * Starts listening on the port from {@link #getPort()} on a new thread. To 67 | * avoid creating a new thread, {@link #run()} can be called directly. 68 | * 69 | * @see {@link #run()}, {@link #stopListening()} 70 | */ 71 | public void listen() { 72 | start(); 73 | } 74 | 75 | /** 76 | * Starts listening on the port from {@link #getPort()} but does so on the 77 | * thread it was invoked from. To run the listener on a new thread, 78 | * {@link #listen()} can be called directly. 79 | * 80 | * @see {@link #listen()}, {@link #stopListening()} 81 | */ 82 | @Override 83 | public void run() { 84 | 85 | // Special cases 86 | if (serverSocket == null) { 87 | return; 88 | } 89 | if (serverSocket.isClosed()) { 90 | try { 91 | this.serverSocket = new ServerSocket( 92 | serverSocket.getLocalPort()); 93 | } catch (IOException e) { 94 | e.printStackTrace(); 95 | } 96 | } 97 | 98 | // Listen for incoming attempts to connect to the server 99 | while (!serverSocket.isClosed()) { 100 | try { 101 | 102 | // Get the connection and handle it 103 | ConnectionHandler conn = new ConnectionHandler( 104 | this, serverSocket.accept()); 105 | conn.start(); 106 | } catch (IOException e) { 107 | e.printStackTrace(); 108 | } catch (Exception e) { 109 | e.printStackTrace(); 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * Tells the listener to stop listening for new connections. 116 | * 117 | * @return true if the connection was torn down successfully. 118 | * @see {@link #listen()}, {@link #run()} 119 | */ 120 | public boolean stopListening() { 121 | 122 | // Special cases 123 | if (serverSocket == null || serverSocket.isClosed()) { 124 | return true; 125 | } 126 | 127 | // Try to close the connection 128 | boolean err = false; 129 | try { 130 | this.serverSocket.close(); 131 | } catch (IOException e) { 132 | e.printStackTrace(); 133 | err = true; 134 | } 135 | 136 | return !err; 137 | } 138 | 139 | /** 140 | * Gets the port the listener will listen on or is listening on. 141 | * 142 | * @return the server socket port. 143 | * @see {@link #setPort(int)} 144 | */ 145 | public int getPort() { 146 | return serverSocket.getLocalPort(); 147 | } 148 | 149 | /** 150 | * Sets the port for this listener to listen on. If the port is less than 151 | * 1, the port will be dynamically allocated. The listener will be stopped 152 | * if it is listening. 153 | * 154 | * @param port the new port to listen on. 155 | * @see {@link #getPort()} 156 | */ 157 | public void setPort(int port) { 158 | 159 | // Stop the server, if it is running 160 | stopListening(); 161 | 162 | // Create the new server socket (the server will need to be restarted) 163 | try { 164 | if (port < 0) { 165 | this.serverSocket = new ServerSocket(0); 166 | } else { 167 | this.serverSocket = new ServerSocket(port); 168 | } 169 | } catch (IOException e) { 170 | e.printStackTrace(); 171 | } 172 | } 173 | 174 | public ServerSocket getServerSocket() { 175 | return serverSocket; 176 | } 177 | 178 | public void setServerSocket(ServerSocket serverSocket) { 179 | this.serverSocket = serverSocket; 180 | } 181 | 182 | public ActionListener getConnectionHandler() { 183 | return connectionHandler; 184 | } 185 | 186 | public void setConnectionHandler(ActionListener connectionHandler) { 187 | this.connectionHandler = connectionHandler; 188 | } 189 | 190 | /** 191 | * Reads all the data that was sent until either the connection is closed 192 | * or the other client stops sending data. 193 | * 194 | * @param socket the connection that should be open. 195 | * @return the data that was read or an empty string otherwise. 196 | */ 197 | public static String read(Socket socket) { 198 | 199 | if (socket == null) { 200 | return ""; 201 | } 202 | 203 | // Read all the data from the stream 204 | String data = ""; 205 | try { 206 | InputStream in = socket.getInputStream(); 207 | BufferedReader br = new BufferedReader(new InputStreamReader(in)); 208 | String line = null; 209 | while ((line = br.readLine()) != null) { 210 | data += line + "\n"; 211 | if (!br.ready()) {break;} 212 | } 213 | if (!data.isEmpty()) { 214 | data = data.substring(0, data.length()-1); 215 | } 216 | } catch (IOException e) { 217 | e.printStackTrace(); 218 | } 219 | 220 | return data; 221 | } 222 | 223 | /** 224 | * Checks to see if a specific port is available. 225 | * 226 | * @param port the port to check for availability. 227 | */ 228 | public static boolean available(int port) { 229 | 230 | if (port < 0 || port > 65535) { 231 | return false; 232 | } 233 | 234 | ServerSocket ss = null; 235 | DatagramSocket ds = null; 236 | try { 237 | ss = new ServerSocket(port); 238 | ss.setReuseAddress(true); 239 | ds = new DatagramSocket(port); 240 | ds.setReuseAddress(true); 241 | return true; 242 | } catch (IOException e) { 243 | } finally { 244 | if (ds != null) { 245 | ds.close(); 246 | } 247 | 248 | if (ss != null) { 249 | try { 250 | ss.close(); 251 | } catch (IOException e) {} 252 | } 253 | } 254 | 255 | return false; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/logic/MoveLogic.java: -------------------------------------------------------------------------------- 1 | /* Name: MoveLogic 2 | * Author: Devon McGrath 3 | * Description: This class simply validates moves. 4 | */ 5 | 6 | package logic; 7 | 8 | import java.awt.Point; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import model.Board; 13 | import model.Game; 14 | 15 | /** 16 | * The {@code MoveLogic} class determines what a valid move is. It fully 17 | * implements all the rules of checkers. 18 | */ 19 | public class MoveLogic { 20 | 21 | /** 22 | * Determines if the specified move is valid based on the rules of checkers. 23 | * 24 | * @param game the game to check against. 25 | * @param startIndex the start index of the move. 26 | * @param endIndex the end index of the move. 27 | * @return true if the move is legal according to the rules of checkers. 28 | * @see {@link #isValidMove(Board, boolean, int, int, int)} 29 | */ 30 | public static boolean isValidMove(Game game, 31 | int startIndex, int endIndex) { 32 | return game == null? false : isValidMove(game.getBoard(), 33 | game.isP1Turn(), startIndex, endIndex, game.getSkipIndex()); 34 | } 35 | 36 | /** 37 | * Determines if the specified move is valid based on the rules of checkers. 38 | * 39 | * @param board the current board to check against. 40 | * @param isP1Turn the flag indicating if it is player 1's turn. 41 | * @param startIndex the start index of the move. 42 | * @param endIndex the end index of the move. 43 | * @param skipIndex the index of the last skip this turn. 44 | * @return true if the move is legal according to the rules of checkers. 45 | * @see {@link #isValidMove(Game, int, int)} 46 | */ 47 | public static boolean isValidMove(Board board, boolean isP1Turn, 48 | int startIndex, int endIndex, int skipIndex) { 49 | 50 | // Basic checks 51 | if (board == null || !Board.isValidIndex(startIndex) || 52 | !Board.isValidIndex(endIndex)) { 53 | return false; 54 | } else if (startIndex == endIndex) { 55 | return false; 56 | } else if (Board.isValidIndex(skipIndex) && skipIndex != startIndex) { 57 | return false; 58 | } 59 | 60 | // Perform the tests to validate the move 61 | if (!validateIDs(board, isP1Turn, startIndex, endIndex)) { 62 | return false; 63 | } else if (!validateDistance(board, isP1Turn, startIndex, endIndex)) { 64 | return false; 65 | } 66 | 67 | // Passed all tests 68 | return true; 69 | } 70 | 71 | /** 72 | * Validates all ID related values for the start, end, and middle (if the 73 | * move is a skip). 74 | * 75 | * @param board the current board to check against. 76 | * @param isP1Turn the flag indicating if it is player 1's turn. 77 | * @param startIndex the start index of the move. 78 | * @param endIndex the end index of the move. 79 | * @return true if and only if all IDs are valid. 80 | */ 81 | private static boolean validateIDs(Board board, boolean isP1Turn, 82 | int startIndex, int endIndex) { 83 | 84 | // Check if end is clear 85 | if (board.get(endIndex) != Board.EMPTY) { 86 | return false; 87 | } 88 | 89 | // Check if proper ID 90 | int id = board.get(startIndex); 91 | if ((isP1Turn && !Board.isBlackChecker(id)) 92 | || (!isP1Turn && !Board.isWhiteChecker(id))) { 93 | return false; 94 | } 95 | 96 | // Check the middle 97 | Point middle = Board.middle(startIndex, endIndex); 98 | int midID = board.get(Board.toIndex(middle)); 99 | if (midID != Board.INVALID && ((!isP1Turn && 100 | !Board.isBlackChecker(midID)) || 101 | (isP1Turn && !Board.isWhiteChecker(midID)))) { 102 | return false; 103 | } 104 | 105 | // Passed all tests 106 | return true; 107 | } 108 | 109 | /** 110 | * Checks that the move is diagonal and magnitude 1 or 2 in the correct 111 | * direction. If the magnitude is not 2 (i.e. not a skip), it checks that 112 | * no skips are available by other checkers of the same player. 113 | * 114 | * @param board the current board to check against. 115 | * @param isP1Turn the flag indicating if it is player 1's turn. 116 | * @param startIndex the start index of the move. 117 | * @param endIndex the end index of the move. 118 | * @return true if and only if the move distance is valid. 119 | */ 120 | private static boolean validateDistance(Board board, boolean isP1Turn, 121 | int startIndex, int endIndex) { 122 | 123 | // Check that it was a diagonal move 124 | Point start = Board.toPoint(startIndex); 125 | Point end = Board.toPoint(endIndex); 126 | int dx = end.x - start.x; 127 | int dy = end.y - start.y; 128 | if (Math.abs(dx) != Math.abs(dy) || Math.abs(dx) > 2 || dx == 0) { 129 | return false; 130 | } 131 | 132 | // Check that it was in the right direction 133 | int id = board.get(startIndex); 134 | if ((id == Board.WHITE_CHECKER && dy > 0) || 135 | (id == Board.BLACK_CHECKER && dy < 0)) { 136 | return false; 137 | } 138 | 139 | // Check that if this is not a skip, there are none available 140 | Point middle = Board.middle(startIndex, endIndex); 141 | int midID = board.get(Board.toIndex(middle)); 142 | if (midID < 0) { 143 | 144 | // Get the correct checkers 145 | List checkers; 146 | if (isP1Turn) { 147 | checkers = board.find(Board.BLACK_CHECKER); 148 | checkers.addAll(board.find(Board.BLACK_KING)); 149 | } else { 150 | checkers = board.find(Board.WHITE_CHECKER); 151 | checkers.addAll(board.find(Board.WHITE_KING)); 152 | } 153 | 154 | // Check if any of them have a skip available 155 | for (Point p : checkers) { 156 | int index = Board.toIndex(p); 157 | if (!MoveGenerator.getSkips(board, index).isEmpty()) { 158 | return false; 159 | } 160 | } 161 | } 162 | 163 | // Passed all tests 164 | return true; 165 | } 166 | 167 | /** 168 | * Checks if the specified checker is safe (i.e. the opponent cannot skip 169 | * the checker). 170 | * 171 | * @param board the current board state. 172 | * @param checker the point where the test checker is located at. 173 | * @return true if and only if the checker at the point is safe. 174 | */ 175 | public static boolean isSafe(Board board, Point checker) { 176 | 177 | // Trivial cases 178 | if (board == null || checker == null) { 179 | return true; 180 | } 181 | int index = Board.toIndex(checker); 182 | if (index < 0) { 183 | return true; 184 | } 185 | int id = board.get(index); 186 | if (id == Board.EMPTY) { 187 | return true; 188 | } 189 | 190 | // Determine if it can be skipped 191 | boolean isBlack = Board.isBlackChecker(id); 192 | List check = new ArrayList<>(); 193 | MoveGenerator.addPoints(check, checker, Board.BLACK_KING, 1); 194 | for (Point p : check) { 195 | int start = Board.toIndex(p); 196 | int tid = board.get(start); 197 | 198 | // Nothing here 199 | if (tid == Board.EMPTY || tid == Board.INVALID) { 200 | continue; 201 | } 202 | 203 | // Check ID 204 | boolean isWhite = Board.isWhiteChecker(tid); 205 | if (isBlack && !isWhite) { 206 | continue; 207 | } 208 | 209 | // Determine if valid skip direction 210 | int dx = (checker.x - p.x) * 2; 211 | int dy = (checker.y - p.y) * 2; 212 | if (!Board.isKingChecker(tid) && (isWhite ^ (dy < 0))) { 213 | continue; 214 | } 215 | int endIndex = Board.toIndex(new Point(p.x + dx, p.y + dy)); 216 | if (MoveGenerator.isValidSkip(board, start, endIndex)) { 217 | return false; 218 | } 219 | } 220 | 221 | return true; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/model/ComputerPlayer.java: -------------------------------------------------------------------------------- 1 | /* Name: ComputerPlayer 2 | * Author: Devon McGrath 3 | * Description: This class represents a computer player which can update the 4 | * game state without user interaction. 5 | */ 6 | 7 | package model; 8 | 9 | import java.awt.Point; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import logic.MoveGenerator; 14 | import logic.MoveLogic; 15 | 16 | /** 17 | * The {@code ComputerPlayer} class represents a computer player and updates 18 | * the board based on a model. 19 | */ 20 | public class ComputerPlayer extends Player { 21 | 22 | /* ----- WEIGHTS ----- */ 23 | /** The weight of being able to skip. */ 24 | private static final double WEIGHT_SKIP = 25; 25 | 26 | /** The weight of being able to skip on next turn. */ 27 | private static final double SKIP_ON_NEXT = 20; 28 | 29 | /** The weight associated with being safe then safe before and after. */ 30 | private static final double SAFE_SAFE = 5; 31 | 32 | /** The weight associated with being safe then unsafe before and after. */ 33 | private static final double SAFE_UNSAFE = -40; 34 | 35 | /** The weight associated with being unsafe then safe before and after. */ 36 | private static final double UNSAFE_SAFE = 40; 37 | 38 | /** The weight associated with being unsafe then unsafe before and after. */ 39 | private static final double UNSAFE_UNSAFE = -40; 40 | 41 | /** The weight of a checker being safe. */ 42 | private static final double SAFE = 3; 43 | 44 | /** The weight of a checker being unsafe. */ 45 | private static final double UNSAFE = -5; 46 | 47 | /** The factor used to multiply some weights when the checker being 48 | * observed is a king. */ 49 | private static final double KING_FACTOR = 2; 50 | /* ------------ */ 51 | 52 | @Override 53 | public boolean isHuman() { 54 | return false; 55 | } 56 | 57 | @Override 58 | public void updateGame(Game game) { 59 | 60 | // Nothing to do 61 | if (game == null || game.isGameOver()) { 62 | return; 63 | } 64 | 65 | // Get the available moves 66 | Game copy = game.copy(); 67 | List moves = getMoves(copy); 68 | 69 | // Determine which one is the best 70 | int n = moves.size(), count = 1; 71 | double bestWeight = Move.WEIGHT_INVALID; 72 | for (int i = 0; i < n; i ++) { 73 | Move m = moves.get(i); 74 | getMoveWeight(copy.copy(), m); 75 | if (m.getWeight() > bestWeight) { 76 | count = 1; 77 | bestWeight = m.getWeight(); 78 | } else if (m.getWeight() == bestWeight) { 79 | count ++; 80 | } 81 | } 82 | 83 | // Randomly select a move 84 | int move = ((int) (Math.random() * count)) % count; 85 | for (int i = 0; i < n; i ++) { 86 | Move m = moves.get(i); 87 | if (bestWeight == m.getWeight()) { 88 | if (move == 0) { 89 | game.move(m.getStartIndex(), m.getEndIndex()); 90 | } else { 91 | move --; 92 | } 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Gets all the available moves and skips for the current player. 99 | * 100 | * @param game the current game state. 101 | * @return a list of valid moves that the player can make. 102 | */ 103 | private List getMoves(Game game) { 104 | 105 | // The next move needs to be a skip 106 | if (game.getSkipIndex() >= 0) { 107 | 108 | List moves = new ArrayList<>(); 109 | List skips = MoveGenerator.getSkips(game.getBoard(), 110 | game.getSkipIndex()); 111 | for (Point end : skips) { 112 | moves.add(new Move(game.getSkipIndex(), Board.toIndex(end))); 113 | } 114 | 115 | return moves; 116 | } 117 | 118 | // Get the checkers 119 | List checkers = new ArrayList<>(); 120 | Board b = game.getBoard(); 121 | if (game.isP1Turn()) { 122 | checkers.addAll(b.find(Board.BLACK_CHECKER)); 123 | checkers.addAll(b.find(Board.BLACK_KING)); 124 | } else { 125 | checkers.addAll(b.find(Board.WHITE_CHECKER)); 126 | checkers.addAll(b.find(Board.WHITE_KING)); 127 | } 128 | 129 | // Determine if there are any skips 130 | List moves = new ArrayList<>(); 131 | for (Point checker : checkers) { 132 | int index = Board.toIndex(checker); 133 | List skips = MoveGenerator.getSkips(b, index); 134 | for (Point end : skips) { 135 | Move m = new Move(index, Board.toIndex(end)); 136 | m.changeWeight(WEIGHT_SKIP); 137 | moves.add(m); 138 | } 139 | } 140 | 141 | // If there are no skips, add the regular moves 142 | if (moves.isEmpty()) { 143 | for (Point checker : checkers) { 144 | int index = Board.toIndex(checker); 145 | List movesEnds = MoveGenerator.getMoves(b, index); 146 | for (Point end : movesEnds) { 147 | moves.add(new Move(index, Board.toIndex(end))); 148 | } 149 | } 150 | } 151 | 152 | return moves; 153 | } 154 | 155 | /** 156 | * Gets the number of skips that can be made in one turn from a given start 157 | * index. 158 | * 159 | * @param game the game state to check against. 160 | * @param startIndex the start index of the skips. 161 | * @param isP1Turn the original player turn flag. 162 | * @return the maximum number of skips available from the given point. 163 | */ 164 | private int getSkipDepth(Game game, int startIndex, boolean isP1Turn) { 165 | 166 | // Trivial case 167 | if (isP1Turn != game.isP1Turn()) { 168 | return 0; 169 | } 170 | 171 | // Recursively get the depth 172 | List skips = MoveGenerator.getSkips(game.getBoard(), startIndex); 173 | int depth = 0; 174 | for (Point end : skips) { 175 | int endIndex = Board.toIndex(end); 176 | game.move(startIndex, endIndex); 177 | int testDepth = getSkipDepth(game, endIndex, isP1Turn); 178 | if (testDepth > depth) { 179 | depth = testDepth; 180 | } 181 | } 182 | 183 | return depth + (skips.isEmpty()? 0 : 1); 184 | } 185 | 186 | /** 187 | * Determines the weight of a move based on a number of factors (e.g. how 188 | * safe the checker is before/after, whether it can take an opponents 189 | * checker after, etc). 190 | * 191 | * @param game the current game state. 192 | * @param m the move to test. 193 | */ 194 | private void getMoveWeight(Game game, Move m) { 195 | 196 | Point start = m.getStart(), end = m.getEnd(); 197 | int startIndex = Board.toIndex(start), endIndex = Board.toIndex(end); 198 | Board b = game.getBoard(); 199 | boolean changed = game.isP1Turn(); 200 | boolean safeBefore = MoveLogic.isSafe(b, start); 201 | 202 | // Set the initial weight 203 | m.changeWeight(getSafetyWeight(b, game.isP1Turn())); 204 | 205 | // Make the move 206 | if (!game.move(m.getStartIndex(), m.getEndIndex())) { 207 | m.setWeight(Move.WEIGHT_INVALID); 208 | return; 209 | } 210 | b = game.getBoard(); 211 | changed = (changed != game.isP1Turn()); 212 | int id = b.get(endIndex); 213 | boolean isKing = Board.isKingChecker(id); 214 | boolean safeAfter = true; 215 | 216 | // Determine if a skip could be made on next move 217 | if (changed) { 218 | safeAfter = MoveLogic.isSafe(b, end); 219 | int depth = getSkipDepth(game, endIndex, !game.isP1Turn()); 220 | if (safeAfter) { 221 | m.changeWeight(SKIP_ON_NEXT * depth * depth); 222 | } else { 223 | m.changeWeight(SKIP_ON_NEXT); 224 | } 225 | } 226 | 227 | // Check how many more skips are available 228 | else { 229 | int depth = getSkipDepth(game, startIndex, game.isP1Turn()); 230 | m.changeWeight(WEIGHT_SKIP * depth * depth); 231 | } 232 | 233 | // Add the weight appropriate to how safe the checker is 234 | if (safeBefore && safeAfter) { 235 | m.changeWeight(SAFE_SAFE); 236 | } else if (!safeBefore && safeAfter) { 237 | m.changeWeight(UNSAFE_SAFE); 238 | } else if (safeBefore && !safeAfter) { 239 | m.changeWeight(SAFE_UNSAFE * (isKing? KING_FACTOR : 1)); 240 | } else { 241 | m.changeWeight(UNSAFE_UNSAFE); 242 | } 243 | m.changeWeight(getSafetyWeight(b, 244 | changed? !game.isP1Turn() : game.isP1Turn())); 245 | } 246 | 247 | /** 248 | * Calculates the 'safety' state of the game for the player specified. The 249 | * player has 'safe' and 'unsafe' checkers, which respectively, cannot and 250 | * can be skipped by the opponent in the next turn. 251 | * 252 | * @param b the board state to check against. 253 | * @param isBlack the flag indicating if black checkers should be observed. 254 | * @return the weight corresponding to how safe the player's checkers are. 255 | */ 256 | private double getSafetyWeight(Board b, boolean isBlack) { 257 | 258 | // Get the checkers 259 | double weight = 0; 260 | List checkers = new ArrayList<>(); 261 | if (isBlack) { 262 | checkers.addAll(b.find(Board.BLACK_CHECKER)); 263 | checkers.addAll(b.find(Board.BLACK_KING)); 264 | } else { 265 | checkers.addAll(b.find(Board.WHITE_CHECKER)); 266 | checkers.addAll(b.find(Board.WHITE_KING)); 267 | } 268 | 269 | // Determine conditions for each checker 270 | for (Point checker : checkers) { 271 | int index = Board.toIndex(checker); 272 | int id = b.get(index); 273 | if (MoveLogic.isSafe(b, checker)) { 274 | weight += SAFE; 275 | } else { 276 | weight += UNSAFE * (Board.isKingChecker(id)? KING_FACTOR : 1); 277 | } 278 | } 279 | 280 | return weight; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/network/CheckersNetworkHandler.java: -------------------------------------------------------------------------------- 1 | /* Name: CheckersNetworkHandler 2 | * Author: Devon McGrath 3 | * Description: This class handles connections between two clients. It receives 4 | * connections and sends responses. 5 | */ 6 | 7 | package network; 8 | 9 | import java.awt.event.ActionEvent; 10 | import java.awt.event.ActionListener; 11 | import java.io.IOException; 12 | import java.io.OutputStream; 13 | import java.net.Socket; 14 | 15 | import model.NetworkPlayer; 16 | import ui.CheckerBoard; 17 | import ui.CheckersWindow; 18 | import ui.NetworkWindow; 19 | import ui.OptionPanel; 20 | 21 | /** 22 | * The {@code CheckersNetworkHandler} class handles incoming connections from 23 | * remote checkers clients. It decides whether connections should be accepted 24 | * and perform an action on this client. It sends two responses: accepted and 25 | * denied. Each response starts with the corresponding string 26 | * {@link #RESPONSE_ACCEPTED} or {@link #RESPONSE_DENIED}. 27 | */ 28 | public class CheckersNetworkHandler implements ActionListener { 29 | 30 | /** The minimum number of characters in the session ID. */ 31 | private static final int MIN_SID_LENGTH = 16; 32 | 33 | /** The max number of characters in the session ID. */ 34 | private static final int MAX_SID_LENGTH = 64; 35 | 36 | /** The start of a response that was accepted. */ 37 | public static final String RESPONSE_ACCEPTED = "ACCEPTED"; 38 | 39 | /** The start of a response that was denied. */ 40 | public static final String RESPONSE_DENIED = "DENIED"; 41 | 42 | /** The flag indicating if this handler is handling a connection to player 43 | * 1 or not. */ 44 | private boolean isPlayer1; 45 | 46 | /** The checkers window. */ 47 | private CheckersWindow window; 48 | 49 | /** The checker board from the checkers window. */ 50 | private CheckerBoard board; 51 | 52 | /** The option panel in the checkers window. */ 53 | private OptionPanel opts; 54 | 55 | public CheckersNetworkHandler(boolean isPlayer1, CheckersWindow window, 56 | CheckerBoard board, OptionPanel opts) { 57 | this.isPlayer1 = isPlayer1; 58 | this.window = window; 59 | this.board = board; 60 | this.opts = opts; 61 | } 62 | 63 | /** 64 | * Handles a new connection from the {@link ConnectionListener}. 65 | */ 66 | @Override 67 | public void actionPerformed(ActionEvent e) { 68 | 69 | // Invalid event 70 | if (e == null || !(e.getSource() instanceof ConnectionHandler)) { 71 | return; 72 | } 73 | 74 | // Get the data from the connection 75 | ConnectionHandler handler = (ConnectionHandler) e.getSource(); 76 | String data = ConnectionListener.read(handler.getSocket()); 77 | data = data.replace("\r\n", "\n"); 78 | 79 | // Unable to handle 80 | if (window == null || board == null || opts == null) { 81 | sendResponse(handler, "Client error: invalid network handler."); 82 | return; 83 | } 84 | 85 | Session s1 = window.getSession1(), s2 = window.getSession2(); 86 | 87 | // Determine if a valid user 88 | String[] lines = data.split("\n"); 89 | String cmd = lines[0].split(" ")[0].toUpperCase(); 90 | String sid = lines.length > 1? lines[1] : ""; 91 | String response = ""; 92 | boolean match = false; 93 | if (isPlayer1) { 94 | match = sid.equals(s1.getSid()); 95 | } else { 96 | match = sid.equals(s2.getSid()); 97 | } 98 | 99 | // A connected client wants to update the board 100 | if (cmd.equals(Command.COMMAND_UPDATE)) { 101 | String newState = (match && lines.length > 2? lines[2] : ""); 102 | response = handleUpdate(newState); 103 | } 104 | 105 | // A client wants to connect to this one 106 | else if (cmd.equals(Command.COMMAND_CONNECT)) { 107 | 108 | // Get the port that was passed (in the SID field) 109 | int port = -1; 110 | try { 111 | port = Integer.parseInt(sid); 112 | } catch (NumberFormatException err) {} 113 | 114 | // Determine if the client attempting to connect is player 1 115 | String isP1 = (lines.length > 2? lines[2] : ""); 116 | boolean remotePlayer1 = isP1.startsWith("1"); 117 | 118 | // Handle the connect request 119 | response = handleConnect(handler.getSocket(), port, remotePlayer1); 120 | } 121 | 122 | // A connected client wants the current game state 123 | else if (cmd.equals(Command.COMMAND_GET)) { 124 | 125 | // Send the board if there was a SID match 126 | if (match) { 127 | response = RESPONSE_ACCEPTED + "\n" 128 | + board.getGame().getGameState(); 129 | } else { 130 | response = RESPONSE_DENIED; 131 | } 132 | } 133 | 134 | // A connected client wants to disconnect 135 | else if (cmd.equals(Command.COMMAND_DISCONNECT)) { 136 | 137 | // Disconnect if SID match 138 | if (match) { 139 | response = RESPONSE_ACCEPTED + "\nClient has been disconnected."; 140 | if (isPlayer1) { 141 | s1.setSid(null); 142 | this.opts.getNetworkWindow1().setCanUpdateConnect(true); 143 | } else { 144 | s2.setSid(null); 145 | this.opts.getNetworkWindow2().setCanUpdateConnect(true); 146 | } 147 | } else { 148 | response = RESPONSE_DENIED + "\nError: cannot disconnect if not connected."; 149 | } 150 | } 151 | 152 | // Invalid command 153 | else { 154 | response = RESPONSE_DENIED + "\nJava Checkers - unknown " 155 | + "command '" + cmd + "'"; 156 | } 157 | 158 | // Send the response to whoever connected 159 | sendResponse(handler, response); 160 | } 161 | 162 | /** 163 | * Handles the update command from a connected client. The update commands 164 | * is used by the other connected client to update the game state after a 165 | * move was made. If both players on this client are network players, then 166 | * the state if forwarded to the other player (effectively making this 167 | * client a router). 168 | * 169 | * @param newState 170 | * @return 171 | */ 172 | private String handleUpdate(String newState) { 173 | 174 | // New state is invalid 175 | if (newState.isEmpty()) { 176 | return RESPONSE_DENIED; 177 | } 178 | 179 | // Update the current client's game state 180 | this.board.setGameState(false, newState, null); 181 | if (!board.getCurrentPlayer().isHuman()) { 182 | board.update(); 183 | } 184 | 185 | // Check if both players are network players 186 | // If so, forward the game state (i.e. this client acts as a router) 187 | if (isPlayer1 && 188 | board.getPlayer2() instanceof NetworkPlayer) { 189 | board.sendGameState(window.getSession2()); 190 | } else if (!isPlayer1 && 191 | board.getPlayer1() instanceof NetworkPlayer) { 192 | board.sendGameState(window.getSession1()); 193 | } 194 | 195 | return RESPONSE_ACCEPTED; 196 | } 197 | 198 | /** 199 | * Checks if the client connect request can be satisfied. A connection 200 | * request can be satisfied for the player if there is no connected client 201 | * and the client is the correct player (e.g. player 1 can't connect to 202 | * player 1 on this client as the game would not be able to be played). 203 | * 204 | * @param s the socket that the remote client used to connect. 205 | * @param port the port that the remote client sent in the request. 206 | * @param remotePlayer1 the flag indicating if the remote player is player 1. 207 | * @return the resulting response to send to the remote client. 208 | */ 209 | private String handleConnect(Socket s, int port, boolean remotePlayer1) { 210 | 211 | // Check if there is someone already connected 212 | Session s1 = window.getSession1(), s2 = window.getSession2(); 213 | String sid1 = s1.getSid(); 214 | String sid2 = s2.getSid(); 215 | if ((isPlayer1 && sid1 != null && !sid1.isEmpty()) || 216 | (!isPlayer1 && sid2 != null && !sid2.isEmpty())) { 217 | return RESPONSE_DENIED + "\nError: user already connected."; 218 | } 219 | 220 | // Check that it is a valid connection 221 | if (!(isPlayer1 ^ remotePlayer1)) { 222 | return RESPONSE_DENIED + "\nError: the other client is already " 223 | + "player " + (remotePlayer1? "1." : "2."); 224 | } 225 | String host = s.getInetAddress().getHostAddress(); 226 | if (host.equals("127.0.0.1")) { 227 | if ((isPlayer1 && port == s2.getSourcePort()) || 228 | (!isPlayer1 && port == s1.getSourcePort())) { 229 | return RESPONSE_DENIED + "\nError: the client cannot connect " 230 | + "to itself."; 231 | } 232 | } 233 | 234 | // Update the connection 235 | String sid = generateSessionID(); 236 | Session session = isPlayer1? s1 : s2; 237 | NetworkWindow win = (isPlayer1? 238 | opts.getNetworkWindow1() : opts.getNetworkWindow2()); 239 | session.setSid(sid); 240 | session.setDestinationHost(host); 241 | session.setDestinationPort(port); 242 | 243 | // Update the UI 244 | win.setDestinationHost(host); 245 | win.setDestinationPort(port); 246 | win.setCanUpdateConnect(false); 247 | win.setMessage(" Connected to " + host + ":" + port + "."); 248 | 249 | return RESPONSE_ACCEPTED + "\n" + sid + "\nSuccessfully connected."; 250 | } 251 | 252 | /** 253 | * Sends a response to the connection handler's connection, if it is not 254 | * closed. 255 | * 256 | * @param handler the connection handler to send the response to. 257 | * @param response the response data to send. 258 | */ 259 | private static void sendResponse(ConnectionHandler handler, 260 | String response) { 261 | 262 | // Trivial cases 263 | if (handler == null) { 264 | return; 265 | } 266 | Socket s = handler.getSocket(); 267 | if (s == null || s.isClosed()) { 268 | return; 269 | } 270 | if (response == null) { 271 | response = ""; 272 | } 273 | 274 | // Write the response and close the connection 275 | try (OutputStream os = s.getOutputStream()) { 276 | os.write(response.getBytes()); 277 | os.flush(); 278 | } catch (IOException e) { 279 | e.printStackTrace(); 280 | } finally { 281 | 282 | // Close the socket 283 | try { 284 | s.close(); 285 | } catch (IOException e) { 286 | e.printStackTrace(); 287 | } 288 | } 289 | } 290 | 291 | /** 292 | * Generates a session ID of a random length with random characters. The 293 | * session ID itself is guaranteed to be at least {@value #MIN_SID_LENGTH} 294 | * characters long, but not longer than {@value #MAX_SID_LENGTH}. 295 | * 296 | * @return a randomly generated SID. 297 | */ 298 | private static String generateSessionID() { 299 | 300 | // Generate a string of random length 301 | String sid = ""; 302 | int chars = (int) ((MAX_SID_LENGTH - MIN_SID_LENGTH) * Math.random()) 303 | + MIN_SID_LENGTH; 304 | for (int i = 0; i < chars; i ++) { 305 | 306 | // Generate a character in a random range 307 | int t = (int) (4 * Math.random()); 308 | int min = 32, max = 48; 309 | if (t == 1) { 310 | min = 48; 311 | max = 65; 312 | } else if (t == 2) { 313 | min = 65; 314 | max = 97; 315 | } else if (t == 3) { 316 | min = 97; 317 | max = 125; 318 | } 319 | char randChar = (char) ((Math.random() * (max - min)) + min); 320 | sid += randChar; 321 | } 322 | 323 | return sid; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/ui/OptionPanel.java: -------------------------------------------------------------------------------- 1 | /* Name: OptionPanel 2 | * Author: Devon McGrath 3 | * Description: This class is a user interface to interact with a checkers 4 | * game window. 5 | */ 6 | 7 | package ui; 8 | 9 | import java.awt.FlowLayout; 10 | import java.awt.GridLayout; 11 | import java.awt.event.ActionEvent; 12 | import java.awt.event.ActionListener; 13 | 14 | import javax.swing.JButton; 15 | import javax.swing.JComboBox; 16 | import javax.swing.JLabel; 17 | import javax.swing.JPanel; 18 | 19 | import model.ComputerPlayer; 20 | import model.HumanPlayer; 21 | import model.NetworkPlayer; 22 | import model.Player; 23 | import network.CheckersNetworkHandler; 24 | import network.Command; 25 | import network.ConnectionListener; 26 | import network.Session; 27 | 28 | /** 29 | * The {@code OptionPanel} class provides a user interface component to control 30 | * options for the game of checkers being played in the window. 31 | */ 32 | public class OptionPanel extends JPanel { 33 | 34 | private static final long serialVersionUID = -4763875452164030755L; 35 | 36 | /** The checkers window to update when an option is changed. */ 37 | private CheckersWindow window; 38 | 39 | /** The button that when clicked, restarts the game. */ 40 | private JButton restartBtn; 41 | 42 | /** The combo box that changes what type of player player 1 is. */ 43 | private JComboBox player1Opts; 44 | 45 | /** The network options for player 1. */ 46 | private NetworkWindow player1Net; 47 | 48 | /** The button to perform an action based on the type of player. */ 49 | private JButton player1Btn; 50 | 51 | /** The combo box that changes what type of player player 2 is. */ 52 | private JComboBox player2Opts; 53 | 54 | /** The network options for player 2. */ 55 | private NetworkWindow player2Net; 56 | 57 | /** The button to perform an action based on the type of player. */ 58 | private JButton player2Btn; 59 | 60 | /** 61 | * Creates a new option panel for the specified checkers window. 62 | * 63 | * @param window the window with the game of checkers to update. 64 | */ 65 | public OptionPanel(CheckersWindow window) { 66 | super(new GridLayout(0, 1)); 67 | 68 | this.window = window; 69 | 70 | // Initialize the components 71 | OptionListener ol = new OptionListener(); 72 | final String[] playerTypeOpts = {"Human", "Computer", "Network"}; 73 | this.restartBtn = new JButton("Restart"); 74 | this.player1Opts = new JComboBox<>(playerTypeOpts); 75 | this.player2Opts = new JComboBox<>(playerTypeOpts); 76 | this.restartBtn.addActionListener(ol); 77 | this.player1Opts.addActionListener(ol); 78 | this.player2Opts.addActionListener(ol); 79 | JPanel top = new JPanel(new FlowLayout(FlowLayout.CENTER)); 80 | JPanel middle = new JPanel(new FlowLayout(FlowLayout.LEFT)); 81 | JPanel bottom = new JPanel(new FlowLayout(FlowLayout.LEFT)); 82 | this.player1Net = new NetworkWindow(ol); 83 | this.player1Net.setTitle("Player 1 - Configure Network"); 84 | this.player2Net = new NetworkWindow(ol); 85 | this.player2Net.setTitle("Player 2 - Configure Network"); 86 | this.player1Btn = new JButton("Set Connection"); 87 | this.player1Btn.addActionListener(ol); 88 | this.player1Btn.setVisible(false); 89 | this.player2Btn = new JButton("Set Connection"); 90 | this.player2Btn.addActionListener(ol); 91 | this.player2Btn.setVisible(false); 92 | 93 | // Add components to the layout 94 | top.add(restartBtn); 95 | middle.add(new JLabel("(black) Player 1: ")); 96 | middle.add(player1Opts); 97 | middle.add(player1Btn); 98 | bottom.add(new JLabel("(white) Player 2: ")); 99 | bottom.add(player2Opts); 100 | bottom.add(player2Btn); 101 | this.add(top); 102 | this.add(middle); 103 | this.add(bottom); 104 | } 105 | 106 | public CheckersWindow getWindow() { 107 | return window; 108 | } 109 | 110 | public void setWindow(CheckersWindow window) { 111 | this.window = window; 112 | } 113 | 114 | public void setNetworkWindowMessage(boolean forPlayer1, String msg) { 115 | if (forPlayer1) { 116 | this.player1Net.setMessage(msg); 117 | } else { 118 | this.player2Net.setMessage(msg); 119 | } 120 | } 121 | 122 | public NetworkWindow getNetworkWindow1() { 123 | return player1Net; 124 | } 125 | 126 | public NetworkWindow getNetworkWindow2() { 127 | return player2Net; 128 | } 129 | 130 | private void handleNetworkUpdate(NetworkWindow win, ActionEvent e) { 131 | 132 | if (win == null || window == null || e == null) { 133 | return; 134 | } 135 | 136 | // Get the info 137 | int srcPort = win.getSourcePort(), destPort = win.getDestinationPort(); 138 | String destHost = win.getDestinationHost(); 139 | boolean isPlayer1 = (win == player1Net); 140 | Session s = (isPlayer1? window.getSession1() : window.getSession2()); 141 | 142 | // Setting new port to listen on 143 | if (e.getID() == NetworkWindow.LISTEN_BUTTON) { 144 | 145 | // Validate the port 146 | if (srcPort < 1025 || srcPort > 65535) { 147 | win.setMessage(" Error: source port must be" 148 | + " between 1025 and 65535. "); 149 | return; 150 | } 151 | if (!ConnectionListener.available(srcPort)) { 152 | win.setMessage(" Error: source port " + srcPort+ " is not available."); 153 | return; 154 | } 155 | 156 | // Update the server if necessary 157 | if (s.getListener().getPort() != srcPort) { 158 | s.getListener().stopListening(); 159 | } 160 | s.getListener().setPort(srcPort); 161 | s.getListener().listen(); 162 | win.setMessage(" This client is listening on port " + srcPort); 163 | win.setCanUpdateListen(false); 164 | win.setCanUpdateConnect(true); 165 | } 166 | 167 | // Try to connect 168 | else if (e.getID() == NetworkWindow.CONNECT_BUTTON) { 169 | 170 | // Validate the port and host 171 | if (destPort < 1025 || destPort > 65535) { 172 | win.setMessage(" Error: destination port must be " 173 | + "between 1025 and 65535. "); 174 | return; 175 | } 176 | if (destHost == null || destHost.isEmpty()) { 177 | destHost = "127.0.0.1"; 178 | } 179 | 180 | // Connect to the proposed host 181 | Command connect = new Command(Command.COMMAND_CONNECT, 182 | win.getSourcePort() + "", isPlayer1? "1" : "0"); 183 | String response = connect.send(destHost, destPort); 184 | 185 | // No response 186 | if (response.isEmpty()) { 187 | win.setMessage(" Error: could not connect to " + destHost + 188 | ":" + destPort + "."); 189 | } 190 | 191 | // It was a valid client, but refused to connect 192 | else if (response.startsWith(CheckersNetworkHandler.RESPONSE_DENIED)) { 193 | String[] lines = response.split("\n"); 194 | String errMsg = lines.length > 1? lines[1] : ""; 195 | if (errMsg.isEmpty()) { 196 | win.setMessage(" Error: the other client refused to connect."); 197 | } else { 198 | win.setMessage(" " + errMsg); 199 | } 200 | } 201 | 202 | // The connection was accepted by the checkers client 203 | else if (response.startsWith(CheckersNetworkHandler.RESPONSE_ACCEPTED)){ 204 | 205 | // Update the session 206 | s.setDestinationHost(destHost); 207 | s.setDestinationPort(destPort); 208 | win.setMessage(" Successfully started a session with " + 209 | destHost + ":" + destPort + "."); 210 | win.setCanUpdateConnect(false); 211 | 212 | // Update the SID 213 | String[] lines = response.split("\n"); 214 | String sid = lines.length > 1? lines[1] : ""; 215 | s.setSid(sid); 216 | 217 | // Get the new game state 218 | Command get = new Command(Command.COMMAND_GET, sid, null); 219 | response = get.send(destHost, destPort); 220 | lines = response.split("\n"); 221 | String state = lines.length > 1? lines[1] : ""; 222 | window.setGameState(state); 223 | } 224 | 225 | // General error, maybe the user tried a web server and 226 | // the response is an HTTP response 227 | else { 228 | win.setMessage(" Error: you tried to connect to a host and " 229 | + "port that isn't running a checkers client."); 230 | } 231 | } 232 | } 233 | 234 | /** 235 | * Gets a new instance of the type of player selected for the specified 236 | * combo box. 237 | * 238 | * @param playerOpts the combo box with the player options. 239 | * @return a new instance of a {@link model.Player} object that corresponds 240 | * with the type of player selected. 241 | */ 242 | private static Player getPlayer(JComboBox playerOpts) { 243 | 244 | Player player = new HumanPlayer(); 245 | if (playerOpts == null) { 246 | return player; 247 | } 248 | 249 | // Determine the type 250 | String type = "" + playerOpts.getSelectedItem(); 251 | if (type.equals("Computer")) { 252 | player = new ComputerPlayer(); 253 | } else if (type.equals("Network")) { 254 | player = new NetworkPlayer(); 255 | } 256 | 257 | return player; 258 | } 259 | 260 | /** 261 | * The {@code OptionListener} class responds to the components within the 262 | * option panel when they are clicked/updated. 263 | */ 264 | private class OptionListener implements ActionListener { 265 | 266 | @Override 267 | public void actionPerformed(ActionEvent e) { 268 | 269 | // No window to update 270 | if (window == null) { 271 | return; 272 | } 273 | 274 | Object src = e.getSource(); 275 | 276 | // Handle the user action 277 | JButton btn = null; 278 | boolean isNetwork = false, isP1 = true; 279 | Session s = null; 280 | if (src == restartBtn) { 281 | window.restart(); 282 | window.getBoard().updateNetwork(); 283 | } else if (src == player1Opts) { 284 | Player player = getPlayer(player1Opts); 285 | window.setPlayer1(player); 286 | isNetwork = (player instanceof NetworkPlayer); 287 | btn = player1Btn; 288 | s = window.getSession1(); 289 | } else if (src == player2Opts) { 290 | Player player = getPlayer(player2Opts); 291 | window.setPlayer2(player); 292 | isNetwork = (player instanceof NetworkPlayer); 293 | btn = player2Btn; 294 | s = window.getSession2(); 295 | isP1 = false; 296 | } else if (src == player1Btn) { 297 | player1Net.setVisible(true); 298 | } else if (src == player2Btn) { 299 | player2Net.setVisible(true); 300 | } 301 | 302 | // Handle a network update 303 | else if (src == player1Net || src == player2Net) { 304 | handleNetworkUpdate((NetworkWindow) src, e); 305 | } 306 | 307 | // Update UI 308 | if (btn != null) { 309 | 310 | // Disconnect if required 311 | String sid = s.getSid(); 312 | if (!isNetwork && btn.isVisible() && 313 | sid != null && !sid.isEmpty()) { 314 | 315 | // Send the request 316 | Command disconnect = new Command( 317 | Command.COMMAND_DISCONNECT, sid); 318 | disconnect.send( 319 | s.getDestinationHost(), s.getDestinationPort()); 320 | 321 | // Update the session 322 | s.setSid(null); 323 | NetworkWindow win = isP1? player1Net : player2Net; 324 | win.setCanUpdateConnect(true); 325 | } 326 | 327 | // Update the UI 328 | btn.setVisible(isNetwork); 329 | btn.repaint(); 330 | } 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/ui/NetworkWindow.java: -------------------------------------------------------------------------------- 1 | /* Name: NetworkWindow 2 | * Author: Devon McGrath 3 | * Description: This class is a window that contains connection settings for a 4 | * player. 5 | */ 6 | 7 | package ui; 8 | 9 | import java.awt.FlowLayout; 10 | import java.awt.GridLayout; 11 | import java.awt.event.ActionEvent; 12 | import java.awt.event.ActionListener; 13 | 14 | import javax.swing.JButton; 15 | import javax.swing.JFrame; 16 | import javax.swing.JLabel; 17 | import javax.swing.JPanel; 18 | import javax.swing.JTextField; 19 | 20 | /** 21 | * The {@code NetworkWindow} class is used as a way to get input from the user 22 | * in making network connections between checkers clients. It specifies the 23 | * port that the client should listen on and the destination (remote) client's 24 | * host name or IP and port that it is listening on. 25 | *

26 | * The network window can be provided with an action listener through 27 | * {@link #setActionListener(ActionListener)}. This action listener will get 28 | * invoked when either the "Listen" or "Connect" buttons are pressed. The 29 | * {@link ActionEvent} itself contains the network window as the source object 30 | * and the ID is either {@link #LISTEN_BUTTON} or {@link #CONNECT_BUTTON} 31 | * (depending on which button was clicked). 32 | *

33 | * This class does not implement any network logic. It only provides an 34 | * interface to get the required network settings. 35 | */ 36 | public class NetworkWindow extends JFrame { 37 | 38 | private static final long serialVersionUID = -3680869784531557351L; 39 | 40 | /** The default width for the network window. */ 41 | public static final int DEFAULT_WIDTH = 480; 42 | 43 | /** The default height for the network window. */ 44 | public static final int DEFAULT_HEIGHT = 140; 45 | 46 | /** The default title for the network window. */ 47 | public static final String DEFAULT_TITLE = "Configure Network"; 48 | 49 | /** The ID sent to the action listener when the connect button is clicked. */ 50 | public static final int CONNECT_BUTTON = 0; 51 | 52 | /** The ID sent to the action listener when the listen button is clicked. */ 53 | public static final int LISTEN_BUTTON = 1; 54 | 55 | /** The text field for the source port. */ 56 | private JTextField srcPort; 57 | 58 | /** The text field for the destination host name or IP. */ 59 | private JTextField destHost; 60 | 61 | /** The text field for the destination port. */ 62 | private JTextField destPort; 63 | 64 | /** The button that is used to indicate that the client should start 65 | * listening on the port specified in {@link #srcPort}. */ 66 | private JButton listen; 67 | 68 | /** The button that is used to indicate that the client should attempt to 69 | * connect to the remote host/port specified in {@link #destHost} and 70 | * {@link #destPort}. */ 71 | private JButton connect; 72 | 73 | /** The panel containing all the components for this client's settings. */ 74 | private JPanel src; 75 | 76 | /** The panel containing all the components for the remote client's 77 | * settings. */ 78 | private JPanel dest; 79 | 80 | /** The label to display the message on the window. */ 81 | private JLabel msg; 82 | 83 | /** The action listener that is invoked when "Listen" or "Connect" is 84 | * clicked. */ 85 | private ActionListener actionListener; 86 | 87 | /** 88 | * Creates a network window with all blank fields and no action listener. 89 | * The action listener should be set through 90 | * {@link #setActionListener(ActionListener)} in order to handle the 91 | * network settings. 92 | * 93 | * @see {@link #NetworkWindow(ActionListener)}, 94 | * {@link #NetworkWindow(ActionListener, int, String, int)} 95 | */ 96 | public NetworkWindow() { 97 | super(DEFAULT_TITLE); 98 | super.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 99 | super.setLocationByPlatform(true); 100 | init(); 101 | } 102 | 103 | /** 104 | * Creates a network window with all blank fields and an action listener to 105 | * receive events when buttons are clicked. 106 | * 107 | * @param actionListener the action listener to listen for events. 108 | */ 109 | public NetworkWindow(ActionListener actionListener) { 110 | this(); 111 | this.actionListener = actionListener; 112 | } 113 | 114 | /** 115 | * Creates a network window with all the fields already completed and an 116 | * action listener to receive events when buttons are clicked. 117 | * 118 | * @param actionListener the action listener to listen for events. 119 | * @param srcPort the source port value. 120 | * @param destHost the destination host name or IP. 121 | * @param destPort the destination port value. 122 | */ 123 | public NetworkWindow(ActionListener actionListener, int srcPort, 124 | String destHost, int destPort) { 125 | this(); 126 | this.actionListener = actionListener; 127 | setSourcePort(srcPort); 128 | setDestinationHost(destHost); 129 | setDestinationPort(destPort); 130 | } 131 | 132 | /** Initializes the components to display in the window. */ 133 | private void init() { 134 | 135 | // Setup the components 136 | this.getContentPane().setLayout(new GridLayout(3, 1)); 137 | this.srcPort = new JTextField(4); 138 | this.destHost = new JTextField(11); 139 | this.destHost.setText("127.0.0.1"); 140 | this.destPort = new JTextField(4); 141 | this.listen = new JButton("Listen"); 142 | this.listen.addActionListener(new ButtonListener()); 143 | this.connect = new JButton("Connect"); 144 | this.connect.addActionListener(new ButtonListener()); 145 | this.src = new JPanel(new FlowLayout(FlowLayout.LEFT)); 146 | this.dest = new JPanel(new FlowLayout(FlowLayout.LEFT)); 147 | this.msg = new JLabel(); 148 | this.src.add(new JLabel("Source port:")); 149 | this.src.add(srcPort); 150 | this.src.add(listen); 151 | this.dest.add(new JLabel("Destination host/port:")); 152 | this.dest.add(destHost); 153 | this.dest.add(destPort); 154 | this.dest.add(connect); 155 | setCanUpdateConnect(false); 156 | 157 | // Add tool tips 158 | this.srcPort.setToolTipText("Source port to listen for " 159 | + "updates (1025 - 65535)"); 160 | this.destPort.setToolTipText("Destination port to listen for " 161 | + "updates (1025 - 65535)"); 162 | this.destHost.setToolTipText("The destination host to send " 163 | + "updates to (e.g. localhost)"); 164 | 165 | createLayout(null); 166 | } 167 | 168 | /** 169 | * Creates or updates the layout with an optional message. 170 | * 171 | * @param msg the message to display. 172 | */ 173 | private void createLayout(String msg) { 174 | 175 | this.getContentPane().removeAll(); 176 | 177 | // Add the appropriate components 178 | this.getContentPane().add(src); 179 | this.getContentPane().add(dest); 180 | this.msg.setText(msg); 181 | this.getContentPane().add(this.msg); 182 | this.msg.setVisible(false); 183 | this.msg.setVisible(true); 184 | } 185 | 186 | /** 187 | * Updates the state of the components required to update the port this 188 | * client is listening on. 189 | * 190 | * @param canUpdate true if the listen components should be enabled. 191 | */ 192 | public void setCanUpdateListen(boolean canUpdate) { 193 | this.srcPort.setEnabled(canUpdate); 194 | this.listen.setEnabled(canUpdate); 195 | } 196 | 197 | /** 198 | * Updates the state of the components required to make a remote 199 | * connection to another checkers client. 200 | * 201 | * @param canUpdate true if the connect components should be enabled. 202 | */ 203 | public void setCanUpdateConnect(boolean canUpdate) { 204 | this.destHost.setEnabled(canUpdate); 205 | this.destPort.setEnabled(canUpdate); 206 | this.connect.setEnabled(canUpdate); 207 | } 208 | 209 | /** 210 | * Gets the action listener that is invoked when either the "Listen" or 211 | * "Connect" button is pressed. 212 | * 213 | * @return the action listener for the buttons. 214 | * @see {@link #setActionListener(ActionListener)} 215 | */ 216 | public ActionListener getActionListener() { 217 | return actionListener; 218 | } 219 | 220 | /** 221 | * Sets the action listener that is invoked when either the "Listen" or 222 | * "Connect" button is pressed. 223 | * 224 | * @param actionListener the action listener to receive button events. 225 | * @see {@link #getActionListener()} 226 | */ 227 | public void setActionListener(ActionListener actionListener) { 228 | this.actionListener = actionListener; 229 | } 230 | 231 | /** 232 | * Gets the source port that the user entered in the corresponding text 233 | * field. The source port is the port that this client will be listening 234 | * on for connections from other remote clients. 235 | * 236 | * @return the parsed source port text that the user entered. 237 | * @see {@link #setSourcePort(int)} 238 | */ 239 | public int getSourcePort() { 240 | return parseField(srcPort); 241 | } 242 | 243 | /** 244 | * Sets the source port entered in the corresponding text field. The source 245 | * port is the port that this client will be listening on for connections 246 | * from other remote clients. 247 | * 248 | * @param port the source port. 249 | * @see {@link #getSourcePort()} 250 | */ 251 | public void setSourcePort(int port) { 252 | this.srcPort.setText("" + port); 253 | } 254 | 255 | /** 256 | * Gets the destination host entered in the corresponding text field. The 257 | * destination host is the IP or host name of the remote client to connect 258 | * to. 259 | * 260 | * @return the destination host text entered by the user. 261 | * @see {@link #setDestinationHost(String)}, 262 | * {@link #getDestinationPort()}, {@link #setDestinationPort(int)} 263 | */ 264 | public String getDestinationHost() { 265 | return destHost.getText(); 266 | } 267 | 268 | /** 269 | * Sets the destination host text in the corresponding text field. The 270 | * destination host is the IP or host name of the remote client to connect 271 | * to. 272 | * 273 | * @param host the host name or IP of the destination host. 274 | * @see {@link #getDestinationHost()}, {@link #getDestinationPort()}, 275 | * {@link #setDestinationPort(int)} 276 | */ 277 | public void setDestinationHost(String host) { 278 | this.destHost.setText(host); 279 | } 280 | 281 | /** 282 | * Gets the destination port text entered in the corresponding text field. 283 | * The destination port is the port on the remote client to connect to. 284 | * 285 | * @return the parsed destination port text that the user entered. 286 | * @see {@link #setDestinationPort(int)}, {@link #getDestinationHost()}, 287 | * {@link #setDestinationHost(String)} 288 | */ 289 | public int getDestinationPort() { 290 | return parseField(destPort); 291 | } 292 | 293 | /** 294 | * Sets the destination port text in the corresponding text field. 295 | * The destination port is the port on the remote client to connect to. 296 | * 297 | * @param port the destination port. 298 | */ 299 | public void setDestinationPort(int port) { 300 | this.destPort.setText("" + port); 301 | } 302 | 303 | /** 304 | * Gets the message text being displayed on the window. 305 | * 306 | * @return the message being displayed. 307 | * @see {@link #setMessage(String)} 308 | */ 309 | public String getMessage() { 310 | return msg.getText(); 311 | } 312 | 313 | /** 314 | * Sets the message to display on the window and updates the user 315 | * interface. 316 | * 317 | * @param message the message to display. 318 | * @see {@link #getMessage()} 319 | */ 320 | public void setMessage(String message) { 321 | createLayout(message); 322 | } 323 | 324 | /** 325 | * Attempts to parse the specified text field value to an integer. 326 | * 327 | * @param tf the text field to parse. 328 | * @return the integer value parsed from the text field or 0 if an error 329 | * occurred. 330 | */ 331 | private static int parseField(JTextField tf) { 332 | 333 | if (tf == null) { 334 | return 0; 335 | } 336 | 337 | // Try to parse the text input 338 | int val = 0; 339 | try { 340 | val = Integer.parseInt(tf.getText()); 341 | } catch (NumberFormatException e) {} 342 | 343 | return val; 344 | } 345 | 346 | /** 347 | * The {@code ButtonListener} class listens for button click events from 348 | * any button in the window. 349 | */ 350 | private class ButtonListener implements ActionListener { 351 | 352 | @Override 353 | public void actionPerformed(ActionEvent e) { 354 | 355 | if (actionListener != null) { 356 | JButton src = (JButton) e.getSource(); 357 | ActionEvent event = null; 358 | if (src == listen) { 359 | event = new ActionEvent(NetworkWindow.this, 360 | LISTEN_BUTTON, null); 361 | } else { 362 | event = new ActionEvent(NetworkWindow.this, 363 | CONNECT_BUTTON, null); 364 | } 365 | actionListener.actionPerformed(event); 366 | } 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/model/Board.java: -------------------------------------------------------------------------------- 1 | /* Name: Board 2 | * Author: Devon McGrath 3 | * Description: This class implements an 8x8 checker board. Under standard 4 | * rules, a checker can only move on black tiles, meaning there are only 32 5 | * available tiles. It uses three integers to represent the board, giving 6 | * 3 bits to each black tile. 7 | */ 8 | 9 | package model; 10 | 11 | import java.awt.Point; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * The {@code Board} class represents a game state for checkers. A standard 17 | * checker board is 8 x 8 (64) tiles, alternating white/black. Checkers are 18 | * only allowed on black tiles and can therefore only move diagonally. The 19 | * board is optimized to use as little memory space as possible and only uses 20 | * 3 integers to represent the state of the board (3 bits for each of the 32 21 | * tiles). This makes it fast and efficient to {@link #copy()} the board state. 22 | *

23 | * This class uses integers to represent the state of each tile and 24 | * specifically uses these constants for IDs: {@link #EMPTY}, 25 | * {@link #BLACK_CHECKER}, {@link #WHITE_CHECKER}, {@link #BLACK_KING}, 26 | * {@link #WHITE_KING}. 27 | *

28 | * Tile states can be retrieved through {@link #get(int)} and 29 | * {@link #get(int, int)}. Tile states can be set through 30 | * {@link #set(int, int)} and {@link #set(int, int, int)}. The entire game can 31 | * be reset with {@link #reset()}. 32 | */ 33 | public class Board { 34 | 35 | /** An ID indicating a point was not on the checker board. */ 36 | public static final int INVALID = -1; 37 | 38 | /** The ID of an empty checker board tile. */ 39 | public static final int EMPTY = 0; 40 | 41 | /** The ID of a white checker in the checker board. */ 42 | public static final int BLACK_CHECKER = 4 * 1 + 2 * 1 + 1 * 0; 43 | 44 | /** The ID of a white checker in the checker board. */ 45 | public static final int WHITE_CHECKER = 4 * 1 + 2 * 0 + 1 * 0; 46 | 47 | /** The ID of a black checker that is also a king. */ 48 | public static final int BLACK_KING = 4 * 1 + 2 * 1 + 1 * 1; 49 | 50 | /** The ID of a white checker that is also a king. */ 51 | public static final int WHITE_KING = 4 * 1 + 2 * 0 + 1 * 1; 52 | 53 | /** The current state of the board, represented as three integers. */ 54 | private int[] state; 55 | 56 | /** 57 | * Constructs a new checker game board, pre-filled with a new game state. 58 | */ 59 | public Board() { 60 | reset(); 61 | } 62 | 63 | /** 64 | * Creates an exact copy of the board. Any changes made to the copy will 65 | * not affect the current object. 66 | * 67 | * @return a copy of this checker board. 68 | */ 69 | public Board copy() { 70 | Board copy = new Board(); 71 | copy.state = state.clone(); 72 | return copy; 73 | } 74 | 75 | /** 76 | * Resets the checker board to the original game state with black checkers 77 | * on top and white on the bottom. There are both 12 black checkers and 12 78 | * white checkers. 79 | */ 80 | public void reset() { 81 | 82 | // Reset the state 83 | this.state = new int[3]; 84 | for (int i = 0; i < 12; i ++) { 85 | set(i, BLACK_CHECKER); 86 | set(31 - i, WHITE_CHECKER); 87 | } 88 | } 89 | 90 | /** 91 | * Searches through the checker board and finds black tiles that match the 92 | * specified ID. 93 | * 94 | * @param id the ID to search for. 95 | * @return a list of points on the board with the specified ID. If none 96 | * exist, an empty list is returned. 97 | */ 98 | public List find(int id) { 99 | 100 | // Find all black tiles with matching IDs 101 | List points = new ArrayList<>(); 102 | for (int i = 0; i < 32; i ++) { 103 | if (get(i) == id) { 104 | points.add(toPoint(i)); 105 | } 106 | } 107 | 108 | return points; 109 | } 110 | 111 | /** 112 | * Sets the ID of a black tile on the board at the specified location. 113 | * If the location is not a black tile, nothing is updated. If the ID is 114 | * less than 0, the board at the location will be set to {@link #EMPTY}. 115 | * 116 | * @param x the x-coordinate on the board (from 0 to 7 inclusive). 117 | * @param y the y-coordinate on the board (from 0 to 7 inclusive). 118 | * @param id the new ID to set the black tile to. 119 | * @see {@link #set(int, int)}, {@link #EMPTY}, {@link #BLACK_CHECKER}, 120 | * {@link #WHITE_CHECKER}, {@link #BLACK_KING}, {@link #WHITE_KING} 121 | */ 122 | public void set(int x, int y, int id) { 123 | set(toIndex(x, y), id); 124 | } 125 | 126 | /** 127 | * Sets the ID of a black tile on the board at the specified location. 128 | * If the location is not a black tile, nothing is updated. If the ID is 129 | * less than 0, the board at the location will be set to {@link #EMPTY}. 130 | * 131 | * @param index the index of the black tile (from 0 to 31 inclusive). 132 | * @param id the new ID to set the black tile to. 133 | * @see {@link #set(int, int, int)}, {@link #EMPTY}, {@link #BLACK_CHECKER}, 134 | * {@link #WHITE_CHECKER}, {@link #BLACK_KING}, {@link #WHITE_KING} 135 | */ 136 | public void set(int index, int id) { 137 | 138 | // Out of range 139 | if (!isValidIndex(index)) { 140 | return; 141 | } 142 | 143 | // Invalid ID, so just set to EMPTY 144 | if (id < 0) { 145 | id = EMPTY; 146 | } 147 | 148 | // Set the state bits 149 | for (int i = 0; i < state.length; i ++) { 150 | boolean set = ((1 << (state.length - i - 1)) & id) != 0; 151 | this.state[i] = setBit(state[i], index, set); 152 | } 153 | } 154 | 155 | /** 156 | * Gets the ID corresponding to the specified point on the checker board. 157 | * 158 | * @param x the x-coordinate on the board (from 0 to 7 inclusive). 159 | * @param y the y-coordinate on the board (from 0 to 7 inclusive). 160 | * @return the ID at the specified location or {@link #INVALID} if the 161 | * location is not on the board or the location is a white tile. 162 | * @see {@link #get(int)}, {@link #set(int, int)}, 163 | * {@link #set(int, int, int)} 164 | */ 165 | public int get(int x, int y) { 166 | return get(toIndex(x, y)); 167 | } 168 | 169 | /** 170 | * Gets the ID corresponding to the specified point on the checker board. 171 | * 172 | * @param index the index of the black tile (from 0 to 31 inclusive). 173 | * @return the ID at the specified location or {@link #INVALID} if the 174 | * location is not on the board. 175 | * @see {@link #get(int, int)}, {@link #set(int, int)}, 176 | * {@link #set(int, int, int)} 177 | */ 178 | public int get(int index) { 179 | if (!isValidIndex(index)) { 180 | return INVALID; 181 | } 182 | return getBit(state[0], index) * 4 + getBit(state[1], index) * 2 183 | + getBit(state[2], index); 184 | } 185 | 186 | /** 187 | * Converts a black tile index (0 to 31 inclusive) to an (x, y) point, such 188 | * that index 0 is (1, 0), index 1 is (3, 0), ... index 31 is (7, 7). 189 | * 190 | * @param index the index of the black tile to convert to a point. 191 | * @return the (x, y) point corresponding to the black tile index or the 192 | * point (-1, -1) if the index is not between 0 - 31 (inclusive). 193 | * @see {@link #toIndex(int, int)}, {@link #toIndex(Point)} 194 | */ 195 | public static Point toPoint(int index) { 196 | int y = index / 4; 197 | int x = 2 * (index % 4) + (y + 1) % 2; 198 | return !isValidIndex(index)? new Point(-1, -1) : new Point(x, y); 199 | } 200 | 201 | /** 202 | * Converts a point to an index of a black tile on the checker board, such 203 | * that (1, 0) is index 0, (3, 0) is index 1, ... (7, 7) is index 31. 204 | * 205 | * @param x the x-coordinate on the board (from 0 to 7 inclusive). 206 | * @param y the y-coordinate on the board (from 0 to 7 inclusive). 207 | * @return the index of the black tile or -1 if the point is not a black 208 | * tile. 209 | * @see {@link #toIndex(Point)}, {@link #toPoint(int)} 210 | */ 211 | public static int toIndex(int x, int y) { 212 | 213 | // Invalid (x, y) (i.e. not in board, or white tile) 214 | if (!isValidPoint(new Point(x, y))) { 215 | return -1; 216 | } 217 | 218 | return y * 4 + x / 2; 219 | } 220 | 221 | /** 222 | * Converts a point to an index of a black tile on the checker board, such 223 | * that (1, 0) is index 0, (3, 0) is index 1, ... (7, 7) is index 31. 224 | * 225 | * @param p the point to convert to an index. 226 | * @return the index of the black tile or -1 if the point is not a black 227 | * tile. 228 | * @see {@link #toIndex(int, int)}, {@link #toPoint(int)} 229 | */ 230 | public static int toIndex(Point p) { 231 | return (p == null)? -1 : toIndex(p.x, p.y); 232 | } 233 | 234 | /** 235 | * Sets or clears the specified bit in the target value and returns 236 | * the updated value. 237 | * 238 | * @param target the target value to update. 239 | * @param bit the bit to update (from 0 to 31 inclusive). 240 | * @param set true to set the bit, false to clear the bit. 241 | * @return the updated target value with the bit set or cleared. 242 | * @see {@link #getBit(int, int)} 243 | */ 244 | public static int setBit(int target, int bit, boolean set) { 245 | 246 | // Nothing to do 247 | if (bit < 0 || bit > 31) { 248 | return target; 249 | } 250 | 251 | // Set the bit 252 | if (set) { 253 | target |= (1 << bit); 254 | } 255 | 256 | // Clear the bit 257 | else { 258 | target &= (~(1 << bit)); 259 | } 260 | 261 | return target; 262 | } 263 | 264 | /** 265 | * Gets the state of a bit and determines if it is set (1) or not (0). 266 | * 267 | * @param target the target value to get the bit from. 268 | * @param bit the bit to get (from 0 to 31 inclusive). 269 | * @return 1 if and only if the specified bit is set, 0 otherwise. 270 | * @see {@link #setBit(int, int, boolean)} 271 | */ 272 | public static int getBit(int target, int bit) { 273 | 274 | // Out of range 275 | if (bit < 0 || bit > 31) { 276 | return 0; 277 | } 278 | 279 | return (target & (1 << bit)) != 0? 1 : 0; 280 | } 281 | 282 | /** 283 | * Gets the middle point on the checker board between two points. 284 | * 285 | * @param p1 the first point of a black tile on the checker board. 286 | * @param p2 the second point of a black tile on the checker board. 287 | * @return the middle point between two points or (-1, -1) if the points 288 | * are not on the board, are not distance 2 from each other in x and y, 289 | * or are on a white tile. 290 | * @see {@link #middle(int, int)}, {@link #middle(int, int, int, int)} 291 | */ 292 | public static Point middle(Point p1, Point p2) { 293 | 294 | // A point isn't initialized 295 | if (p1 == null || p2 == null) { 296 | return new Point(-1, -1); 297 | } 298 | 299 | return middle(p1.x, p1.y, p2.x, p2.y); 300 | } 301 | 302 | /** 303 | * Gets the middle point on the checker board between two points. 304 | * 305 | * @param index1 the index of the first point (from 0 to 31 inclusive). 306 | * @param index2 the index of the second point (from 0 to 31 inclusive). 307 | * @return the middle point between two points or (-1, -1) if the points 308 | * are not on the board, are not distance 2 from each other in x and y, 309 | * or are on a white tile. 310 | * @see {@link #middle(Point, Point)}, {@link #middle(int, int, int, int)} 311 | */ 312 | public static Point middle(int index1, int index2) { 313 | return middle(toPoint(index1), toPoint(index2)); 314 | } 315 | 316 | /** 317 | * Gets the middle point on the checker board between two points. 318 | * 319 | * @param x1 the x-coordinate of the first point. 320 | * @param y1 the y-coordinate of the first point. 321 | * @param x2 the x-coordinate of the second point. 322 | * @param y2 the y-coordinate of the second point. 323 | * @return the middle point between two points or (-1, -1) if the points 324 | * are not on the board, are not distance 2 from each other in x and y, 325 | * or are on a white tile. 326 | * @see {@link #middle(int, int)}, {@link #middle(Point, Point)} 327 | */ 328 | public static Point middle(int x1, int y1, int x2, int y2) { 329 | 330 | // Check coordinates 331 | int dx = x2 - x1, dy = y2 - y1; 332 | if (x1 < 0 || y1 < 0 || x2 < 0 || y2 < 0 || // Not in the board 333 | x1 > 7 || y1 > 7 || x2 > 7 || y2 > 7) { 334 | return new Point(-1, -1); 335 | } else if (x1 % 2 == y1 % 2 || x2 % 2 == y2 % 2) { // white tile 336 | return new Point(-1, -1); 337 | } else if (Math.abs(dx) != Math.abs(dy) || Math.abs(dx) != 2) { 338 | return new Point(-1, -1); 339 | } 340 | 341 | return new Point(x1 + dx / 2, y1 + dy / 2); 342 | } 343 | 344 | /** 345 | * Checks if an index corresponds to a black tile on the checker board. 346 | * 347 | * @param testIndex the index to check. 348 | * @return true if and only if the index is between 0 and 31 inclusive. 349 | */ 350 | public static boolean isValidIndex(int testIndex) { 351 | return testIndex >= 0 && testIndex < 32; 352 | } 353 | 354 | /** 355 | * Checks if a point corresponds to a black tile on the checker board. 356 | * 357 | * @param testPoint the point to check. 358 | * @return true if and only if the point is on the board, specifically on 359 | * a black tile. 360 | */ 361 | public static boolean isValidPoint(Point testPoint) { 362 | 363 | if (testPoint == null) { 364 | return false; 365 | } 366 | 367 | // Check that it is on the board 368 | final int x = testPoint.x, y = testPoint.y; 369 | if (x < 0 || x > 7 || y < 0 || y > 7) { 370 | return false; 371 | } 372 | 373 | // Check that it is on a black tile 374 | if (x % 2 == y % 2) { 375 | return false; 376 | } 377 | 378 | return true; 379 | } 380 | 381 | /** 382 | * Checks if the specified ID is for a black checker. 383 | * 384 | * @param id the ID to check. 385 | * @return true if the ID corresponds to a {@link #BLACK_CHECKER} or 386 | * a {@link #BLACK_KING} checker. 387 | */ 388 | public static boolean isBlackChecker(int id) { 389 | return id == Board.BLACK_CHECKER || id == Board.BLACK_KING; 390 | } 391 | 392 | /** 393 | * Checks if the specified ID is for a white checker. 394 | * 395 | * @param id the ID to check. 396 | * @return true if the ID corresponds to a {@link #WHITE_CHECKER} or 397 | * a {@link #WHITE_KING} checker. 398 | */ 399 | public static boolean isWhiteChecker(int id) { 400 | return id == Board.WHITE_CHECKER || id == Board.WHITE_KING; 401 | } 402 | 403 | /** 404 | * Checks if the specified ID is for a king checker. 405 | * 406 | * @param id the ID to check. 407 | * @return true if the ID corresponds to a {@link #BLACK_KING} checker or 408 | * a {@link #WHITE_KING} checker. 409 | */ 410 | public static boolean isKingChecker(int id) { 411 | return id == Board.BLACK_KING || id == Board.WHITE_KING; 412 | } 413 | 414 | @Override 415 | public String toString() { 416 | String obj = getClass().getName() + "["; 417 | for (int i = 0; i < 31; i ++) { 418 | obj += get(i) + ", "; 419 | } 420 | obj += get(31); 421 | 422 | return obj + "]"; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/ui/CheckerBoard.java: -------------------------------------------------------------------------------- 1 | /* Name: CheckerBoard 2 | * Author: Devon McGrath 3 | * Description: This class is the graphical user interface representation of 4 | * a checkers game. It is responsible for drawing the checker board and 5 | * allowing moves to be made. It does not provide a method to allow the user to 6 | * change settings of the game or restart it. 7 | */ 8 | 9 | package ui; 10 | 11 | import java.awt.Color; 12 | import java.awt.Font; 13 | import java.awt.Graphics; 14 | import java.awt.Graphics2D; 15 | import java.awt.Point; 16 | import java.awt.RenderingHints; 17 | import java.awt.event.ActionEvent; 18 | import java.awt.event.ActionListener; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | import javax.swing.JButton; 23 | import javax.swing.Timer; 24 | 25 | import logic.MoveGenerator; 26 | import model.Board; 27 | import model.Game; 28 | import model.HumanPlayer; 29 | import model.NetworkPlayer; 30 | import model.Player; 31 | import network.Command; 32 | import network.Session; 33 | 34 | /** 35 | * The {@code CheckerBoard} class is a graphical user interface component that 36 | * is capable of drawing any checkers game state. It also handles player turns. 37 | * For human players, this means interacting with and selecting tiles on the 38 | * checker board. For non-human players, this means using the logic implemented 39 | * by the specified player object itself is used. 40 | */ 41 | public class CheckerBoard extends JButton { 42 | 43 | private static final long serialVersionUID = -6014690893709316364L; 44 | 45 | /** The amount of milliseconds before a computer player takes a move. */ 46 | private static final int TIMER_DELAY = 1000; 47 | 48 | /** The number of pixels of padding between this component's border and the 49 | * actual checker board that is drawn. */ 50 | private static final int PADDING = 16; 51 | 52 | /** The game of checkers that is being played on this component. */ 53 | private Game game; 54 | 55 | /** The window containing this checker board UI component. */ 56 | private CheckersWindow window; 57 | 58 | /** The player in control of the black checkers. */ 59 | private Player player1; 60 | 61 | /** The player in control of the white checkers. */ 62 | private Player player2; 63 | 64 | /** The last point that the current player selected on the checker board. */ 65 | private Point selected; 66 | 67 | /** The flag to determine the colour of the selected tile. If the selection 68 | * is valid, a green colour is used to highlight the tile. Otherwise, a red 69 | * colour is used. */ 70 | private boolean selectionValid; 71 | 72 | /** The colour of the light tiles (by default, this is white). */ 73 | private Color lightTile; 74 | 75 | /** The colour of the dark tiles (by default, this is black). */ 76 | private Color darkTile; 77 | 78 | /** A convenience flag to check if the game is over. */ 79 | private boolean isGameOver; 80 | 81 | /** The timer to control how fast a computer player makes a move. */ 82 | private Timer timer; 83 | 84 | public CheckerBoard(CheckersWindow window) { 85 | this(window, new Game(), null, null); 86 | } 87 | 88 | public CheckerBoard(CheckersWindow window, Game game, 89 | Player player1, Player player2) { 90 | 91 | // Setup the component 92 | super.setBorderPainted(false); 93 | super.setFocusPainted(false); 94 | super.setContentAreaFilled(false); 95 | super.setBackground(Color.LIGHT_GRAY); 96 | this.addActionListener(new ClickListener()); 97 | 98 | // Setup the game 99 | this.game = (game == null)? new Game() : game; 100 | this.lightTile = Color.WHITE; 101 | this.darkTile = Color.BLACK; 102 | this.window = window; 103 | setPlayer1(player1); 104 | setPlayer2(player2); 105 | } 106 | 107 | /** 108 | * Checks if the game is over and redraws the component graphics. 109 | */ 110 | public void update() { 111 | runPlayer(); 112 | this.isGameOver = game.isGameOver(); 113 | repaint(); 114 | } 115 | 116 | private void runPlayer() { 117 | 118 | // Nothing to do 119 | Player player = getCurrentPlayer(); 120 | if (player == null || player.isHuman() || 121 | player instanceof NetworkPlayer) { 122 | return; 123 | } 124 | 125 | // Set a timer to run 126 | this.timer = new Timer(TIMER_DELAY, new ActionListener() { 127 | 128 | @Override 129 | public void actionPerformed(ActionEvent e) { 130 | getCurrentPlayer().updateGame(game); 131 | timer.stop(); 132 | updateNetwork(); 133 | update(); 134 | } 135 | }); 136 | this.timer.start(); 137 | } 138 | 139 | public void updateNetwork() { 140 | 141 | // Get the relevant sessions to send to 142 | List sessions = new ArrayList<>(); 143 | if (player1 instanceof NetworkPlayer) { 144 | sessions.add(window.getSession1()); 145 | } 146 | if (player2 instanceof NetworkPlayer) { 147 | sessions.add(window.getSession2()); 148 | } 149 | 150 | // Send the game update 151 | for (Session s : sessions) { 152 | sendGameState(s); 153 | } 154 | } 155 | 156 | public synchronized boolean setGameState(boolean testValue, 157 | String newState, String expected) { 158 | 159 | // Test the value if requested 160 | if (testValue && !game.getGameState().equals(expected)) { 161 | return false; 162 | } 163 | 164 | // Update the game state 165 | this.game.setGameState(newState); 166 | repaint(); 167 | 168 | return true; 169 | } 170 | 171 | public void sendGameState(Session s) { 172 | 173 | if (s == null) { 174 | return; 175 | } 176 | 177 | // Create the command and send it 178 | Command update = new Command(Command.COMMAND_UPDATE, 179 | s.getSid(), game.getGameState()); 180 | String host = s.getDestinationHost(); 181 | int port = s.getDestinationPort(); 182 | update.send(host, port); 183 | } 184 | 185 | /** 186 | * Draws the current checkers game state. 187 | */ 188 | @Override 189 | public void paint(Graphics g) { 190 | super.paint(g); 191 | 192 | Graphics2D g2d = (Graphics2D) g; 193 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 194 | RenderingHints.VALUE_ANTIALIAS_ON); 195 | Game game = this.game.copy(); 196 | 197 | // Perform calculations 198 | final int BOX_PADDING = 4; 199 | final int W = getWidth(), H = getHeight(); 200 | final int DIM = W < H? W : H, BOX_SIZE = (DIM - 2 * PADDING) / 8; 201 | final int OFFSET_X = (W - BOX_SIZE * 8) / 2; 202 | final int OFFSET_Y = (H - BOX_SIZE * 8) / 2; 203 | final int CHECKER_SIZE = Math.max(0, BOX_SIZE - 2 * BOX_PADDING); 204 | 205 | // Draw checker board 206 | g.setColor(Color.BLACK); 207 | g.drawRect(OFFSET_X - 1, OFFSET_Y - 1, BOX_SIZE * 8 + 1, BOX_SIZE * 8 + 1); 208 | g.setColor(lightTile); 209 | g.fillRect(OFFSET_X, OFFSET_Y, BOX_SIZE * 8, BOX_SIZE * 8); 210 | g.setColor(darkTile); 211 | for (int y = 0; y < 8; y ++) { 212 | for (int x = (y + 1) % 2; x < 8; x += 2) { 213 | g.fillRect(OFFSET_X + x * BOX_SIZE, OFFSET_Y + y * BOX_SIZE, 214 | BOX_SIZE, BOX_SIZE); 215 | } 216 | } 217 | 218 | // Highlight the selected tile if valid 219 | if (Board.isValidPoint(selected)) { 220 | g.setColor(selectionValid? Color.GREEN : Color.RED); 221 | g.fillRect(OFFSET_X + selected.x * BOX_SIZE, 222 | OFFSET_Y + selected.y * BOX_SIZE, 223 | BOX_SIZE, BOX_SIZE); 224 | } 225 | 226 | // Draw the checkers 227 | Board b = game.getBoard(); 228 | for (int y = 0; y < 8; y ++) { 229 | int cy = OFFSET_Y + y * BOX_SIZE + BOX_PADDING; 230 | for (int x = (y + 1) % 2; x < 8; x += 2) { 231 | int id = b.get(x, y); 232 | 233 | // Empty, just skip 234 | if (id == Board.EMPTY) { 235 | continue; 236 | } 237 | 238 | int cx = OFFSET_X + x * BOX_SIZE + BOX_PADDING; 239 | 240 | // Black checker 241 | if (id == Board.BLACK_CHECKER) { 242 | g.setColor(Color.DARK_GRAY); 243 | g.fillOval(cx + 1, cy + 2, CHECKER_SIZE, CHECKER_SIZE); 244 | g.setColor(Color.LIGHT_GRAY); 245 | g.drawOval(cx + 1, cy + 2, CHECKER_SIZE, CHECKER_SIZE); 246 | g.setColor(Color.BLACK); 247 | g.fillOval(cx, cy, CHECKER_SIZE, CHECKER_SIZE); 248 | g.setColor(Color.LIGHT_GRAY); 249 | g.drawOval(cx, cy, CHECKER_SIZE, CHECKER_SIZE); 250 | } 251 | 252 | // Black king 253 | else if (id == Board.BLACK_KING) { 254 | g.setColor(Color.DARK_GRAY); 255 | g.fillOval(cx + 1, cy + 2, CHECKER_SIZE, CHECKER_SIZE); 256 | g.setColor(Color.LIGHT_GRAY); 257 | g.drawOval(cx + 1, cy + 2, CHECKER_SIZE, CHECKER_SIZE); 258 | g.setColor(Color.DARK_GRAY); 259 | g.fillOval(cx, cy, CHECKER_SIZE, CHECKER_SIZE); 260 | g.setColor(Color.LIGHT_GRAY); 261 | g.drawOval(cx, cy, CHECKER_SIZE, CHECKER_SIZE); 262 | g.setColor(Color.BLACK); 263 | g.fillOval(cx - 1, cy - 2, CHECKER_SIZE, CHECKER_SIZE); 264 | } 265 | 266 | // White checker 267 | else if (id == Board.WHITE_CHECKER) { 268 | g.setColor(Color.LIGHT_GRAY); 269 | g.fillOval(cx + 1, cy + 2, CHECKER_SIZE, CHECKER_SIZE); 270 | g.setColor(Color.DARK_GRAY); 271 | g.drawOval(cx + 1, cy + 2, CHECKER_SIZE, CHECKER_SIZE); 272 | g.setColor(Color.WHITE); 273 | g.fillOval(cx, cy, CHECKER_SIZE, CHECKER_SIZE); 274 | g.setColor(Color.DARK_GRAY); 275 | g.drawOval(cx, cy, CHECKER_SIZE, CHECKER_SIZE); 276 | } 277 | 278 | // White king 279 | else if (id == Board.WHITE_KING) { 280 | g.setColor(Color.LIGHT_GRAY); 281 | g.fillOval(cx + 1, cy + 2, CHECKER_SIZE, CHECKER_SIZE); 282 | g.setColor(Color.DARK_GRAY); 283 | g.drawOval(cx + 1, cy + 2, CHECKER_SIZE, CHECKER_SIZE); 284 | g.setColor(Color.LIGHT_GRAY); 285 | g.fillOval(cx, cy, CHECKER_SIZE, CHECKER_SIZE); 286 | g.setColor(Color.DARK_GRAY); 287 | g.drawOval(cx, cy, CHECKER_SIZE, CHECKER_SIZE); 288 | g.setColor(Color.WHITE); 289 | g.fillOval(cx - 1, cy - 2, CHECKER_SIZE, CHECKER_SIZE); 290 | } 291 | 292 | // Any king (add some extra highlights) 293 | if (Board.isKingChecker(id)) { 294 | g.setColor(new Color(255, 240, 0)); 295 | g.drawOval(cx - 1, cy - 2, CHECKER_SIZE, CHECKER_SIZE); 296 | g.drawOval(cx + 1, cy, CHECKER_SIZE - 4, CHECKER_SIZE - 4); 297 | } 298 | } 299 | } 300 | 301 | // Draw the player turn sign 302 | String msg = game.isP1Turn()? "Player 1's turn" : "Player 2's turn"; 303 | int width = g.getFontMetrics().stringWidth(msg); 304 | Color back = game.isP1Turn()? Color.BLACK : Color.WHITE; 305 | Color front = game.isP1Turn()? Color.WHITE : Color.BLACK; 306 | g.setColor(back); 307 | g.fillRect(W / 2 - width / 2 - 5, OFFSET_Y + 8 * BOX_SIZE + 2, 308 | width + 10, 15); 309 | g.setColor(front); 310 | g.drawString(msg, W / 2 - width / 2, OFFSET_Y + 8 * BOX_SIZE + 2 + 12); 311 | 312 | // Draw a game over sign 313 | if (isGameOver) { 314 | g.setFont(new Font("Arial", Font.BOLD, 20)); 315 | msg = "Game Over!"; 316 | width = g.getFontMetrics().stringWidth(msg); 317 | g.setColor(new Color(240, 240, 255)); 318 | g.fillRoundRect(W / 2 - width / 2 - 5, 319 | OFFSET_Y + BOX_SIZE * 4 - 16, 320 | width + 10, 30, 10, 10); 321 | g.setColor(Color.RED); 322 | g.drawString(msg, W / 2 - width / 2, OFFSET_Y + BOX_SIZE * 4 + 7); 323 | } 324 | } 325 | 326 | public Game getGame() { 327 | return game; 328 | } 329 | 330 | public void setGame(Game game) { 331 | this.game = (game == null)? new Game() : game; 332 | } 333 | 334 | public CheckersWindow getWindow() { 335 | return window; 336 | } 337 | 338 | public void setWindow(CheckersWindow window) { 339 | this.window = window; 340 | } 341 | 342 | public Player getPlayer1() { 343 | return player1; 344 | } 345 | 346 | public void setPlayer1(Player player1) { 347 | this.player1 = (player1 == null)? new HumanPlayer() : player1; 348 | if (game.isP1Turn() && !this.player1.isHuman()) { 349 | this.selected = null; 350 | } 351 | } 352 | 353 | public Player getPlayer2() { 354 | return player2; 355 | } 356 | 357 | public void setPlayer2(Player player2) { 358 | this.player2 = (player2 == null)? new HumanPlayer() : player2; 359 | if (!game.isP1Turn() && !this.player2.isHuman()) { 360 | this.selected = null; 361 | } 362 | } 363 | 364 | public Player getCurrentPlayer() { 365 | return game.isP1Turn()? player1 : player2; 366 | } 367 | 368 | public Color getLightTile() { 369 | return lightTile; 370 | } 371 | 372 | public void setLightTile(Color lightTile) { 373 | this.lightTile = (lightTile == null)? Color.WHITE : lightTile; 374 | } 375 | 376 | public Color getDarkTile() { 377 | return darkTile; 378 | } 379 | 380 | public void setDarkTile(Color darkTile) { 381 | this.darkTile = (darkTile == null)? Color.BLACK : darkTile; 382 | } 383 | 384 | /** 385 | * Handles a click on this component at the specified point. If the current 386 | * player is not human, this method does nothing. Otherwise, the selected 387 | * point is updated and a move is attempted if the last click and this one 388 | * both are on black tiles. 389 | * 390 | * @param x the x-coordinate of the click on this component. 391 | * @param y the y-coordinate of the click on this component. 392 | */ 393 | private void handleClick(int x, int y) { 394 | 395 | // The game is over or the current player isn't human 396 | if (isGameOver || !getCurrentPlayer().isHuman()) { 397 | return; 398 | } 399 | 400 | Game copy = game.copy(); 401 | 402 | // Determine what square (if any) was selected 403 | final int W = getWidth(), H = getHeight(); 404 | final int DIM = W < H? W : H, BOX_SIZE = (DIM - 2 * PADDING) / 8; 405 | final int OFFSET_X = (W - BOX_SIZE * 8) / 2; 406 | final int OFFSET_Y = (H - BOX_SIZE * 8) / 2; 407 | x = (x - OFFSET_X) / BOX_SIZE; 408 | y = (y - OFFSET_Y) / BOX_SIZE; 409 | Point sel = new Point(x, y); 410 | 411 | // Determine if a move should be attempted 412 | if (Board.isValidPoint(sel) && Board.isValidPoint(selected)) { 413 | boolean change = copy.isP1Turn(); 414 | String expected = copy.getGameState(); 415 | boolean move = copy.move(selected, sel); 416 | boolean updated = (move? 417 | setGameState(true, copy.getGameState(), expected) : false); 418 | if (updated) { 419 | updateNetwork(); 420 | } 421 | change = (copy.isP1Turn() != change); 422 | this.selected = change? null : sel; 423 | } else { 424 | this.selected = sel; 425 | } 426 | 427 | // Check if the selection is valid 428 | this.selectionValid = isValidSelection( 429 | copy.getBoard(), copy.isP1Turn(), selected); 430 | 431 | update(); 432 | } 433 | 434 | /** 435 | * Checks if a selected point is valid in the context of the current 436 | * player's turn. 437 | * 438 | * @param b the current board. 439 | * @param isP1Turn the flag indicating if it is player 1's turn. 440 | * @param selected the point to test. 441 | * @return true if and only if the selected point is a checker that would 442 | * be allowed to make a move in the current turn. 443 | */ 444 | private boolean isValidSelection(Board b, boolean isP1Turn, Point selected) { 445 | 446 | // Trivial cases 447 | int i = Board.toIndex(selected), id = b.get(i); 448 | if (id == Board.EMPTY || id == Board.INVALID) { // no checker here 449 | return false; 450 | } else if(isP1Turn ^ Board.isBlackChecker(id)) { // wrong checker 451 | return false; 452 | } else if (!MoveGenerator.getSkips(b, i).isEmpty()) { // skip available 453 | return true; 454 | } else if (MoveGenerator.getMoves(b, i).isEmpty()) { // no moves 455 | return false; 456 | } 457 | 458 | // Determine if there is a skip available for another checker 459 | List points = b.find( 460 | isP1Turn? Board.BLACK_CHECKER : Board.WHITE_CHECKER); 461 | points.addAll(b.find( 462 | isP1Turn? Board.BLACK_KING : Board.WHITE_KING)); 463 | for (Point p : points) { 464 | int checker = Board.toIndex(p); 465 | if (checker == i) { 466 | continue; 467 | } 468 | if (!MoveGenerator.getSkips(b, checker).isEmpty()) { 469 | return false; 470 | } 471 | } 472 | 473 | return true; 474 | } 475 | 476 | /** 477 | * The {@code ClickListener} class is responsible for responding to click 478 | * events on the checker board component. It uses the coordinates of the 479 | * mouse relative to the location of the checker board component. 480 | */ 481 | private class ClickListener implements ActionListener { 482 | 483 | @Override 484 | public void actionPerformed(ActionEvent e) { 485 | 486 | // Get the new mouse coordinates and handle the click 487 | Point m = CheckerBoard.this.getMousePosition(); 488 | if (m != null) { 489 | handleClick(m.x, m.y); 490 | } 491 | } 492 | } 493 | } 494 | --------------------------------------------------------------------------------