├── .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 |
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 | 
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 |
--------------------------------------------------------------------------------