The Architecture Diagram given above explains the high-level design of the application. Given below is a quick overview of main components and how they interact with each other.
ChessMaster
is the main invocation of the application. It handles the loading of previous chess games from the storage file and running of the chess Game
instance.
The remaining logic is handled by the following components:
Command
, Move
, ChessPiece
, etc)Our application also uses other classes to store information about the chess game and provide utility functions for the main components to function. This include: Command
, Move
, Coordinate
, Color
, Player
, ChessTile
and ChessPiece(s).
The sequence diagram below illustrates the interactions within the ChessMaster component, when they launch the program.
The user has the option to terminate the game anytime during this interaction with exit
, however, this event is not depicted in the diagram for simplicity.
How does ChessMaster component work:
The sequence diagram below illustrates the interactions within the Game component.
How does the Game component work:
MoveCommand
.Below is a class diagram representing the Command and Parser classes.
In order to handle user input into the program during the game, the Parser
class was implemented.
Below is a sequence diagram describing the process of handling user input passed from Game
:
Parser
works to resolve a player’s input in the following manner:
Parser
is called to parse a command, it returns the relevant Command
object (More precisely,
one of its subclasses e.g. MoveCommand
), which is then executed by Game
.Command
returned, the following may occur:
MoveCommand
, the Command calls parseMove
to instantiate the Move
,
which is passed back to Game
to be executed in the main logic.ShowMovesCommand
, parseAlgebraicCoor
is called to obtain the position of the piece as a
Coordinate
object. The available coordinates are printed using showAvailableCoordinates
, then stored as a String
by getAvailableCoordinatesString()
.CommandResult
and returned to Game
to be handled.parsePromote
is called.Parser
calls getColor
and getPosition
to retrieve relevant data from the ChessPiece
the player wants to promote.ChessPiece
is returned.Parser
also contains methods to fulfil parsing needs in other parts of the program, for instance parseChessPiece
,
which is called while loading the .txt file containing save data, called for each character representing a
singular chess piece. Using a Case statement, it returns the relevant ChessPiece
object depending on the character
(representing the type of piece), and whether it is capitalised (representing colour).
How the parsing works:
Parser
class returns a relevant subclass of the Command
class
(i.e. entering a valid command “XYZ” will cause Parser
to return an XYZCommand
object)Command
subclass contains the relevant methods to execute the specified command
(AbortCommand, ShowCommand, etc.) and inherit from the abstract Command
class.The minimax algorithm is used to determine the best move for the AI to make. It is a recursive algorithm that works by alternatingly minimising opponent scores and maximising CPU scores. The algorithm is implemented in the Minimax
class.
How the minimax algorithm works:
Minimax
class is called by the Game
class to determine the best move for the CPU to make.Minimax
class calls the getPossibleMoves
method in the ChessBoard
class to get all possible moves for the CPU.Minimax
class then calls the getBestMove
method to determine the best move for the CPU to make by maximising CPU score and minimizing the player score.getBestMove
method calls the getBestMove
method recursively to populate the child scores.getBestMove
method returns the best move for the CPU to make.The Move class and its subclasses are responsible for handling the different types of moves in chess. The Move class is a parent class of several move types: CastleMove, EnPassantMove and PromoteMove. The MoveFactory class is responsible for generating the correct move type.
Below is a class diagram representing the parent Move class.
Standard moves are the most common type of moves in chess. They are moves that involve moving a piece from one tile to another without considering special cases. Standard moves are further classified into two types: normal moves and capture moves. This information is implemented in the ChessPiece classes.
How standard moves are generated:
directions
and executed separately with different move functions.Below is a sequence diagram representing the process of generating and executing moves.
A pawn opening is a special type of move that involves moving a pawn from its starting position to another tile two spaces ahead. This move is only valid if the pawn is in its starting position and the tile it is moving to as well as the tile in between is empty.
Castling is a special type of move that involves moving the king and rook at the same time. This move is only valid if the king and rook have not moved before and there are no pieces between them. Castling is further classified into two types: king-side castling and queen-side castling. This information is implemented in the ChessPiece King and Rook classes and executed with the CastleMove and CastleSide classes.
En passant is a special type of move that involves capturing a pawn that has just moved two spaces. This move is only valid if the pawn is in the correct position and the pawn is the last piece to move. This information is implemented in the Pawn class and executed with the EnPassantMove class.
Promotion is a special type of move that involves promoting a pawn to another piece. This move is only valid if the pawn is in the correct position, which is at the other end of the board from which it starts. This information is implemented in the Pawn class and executed with the PromotionMove class.
Below is a class diagram representing the Storage class. The Storage component is responsible for handling the storage and retrieval of chess game state.
Below is a sequence diagram of the storage class. It includes only the more essential methods (createChessmasterFile, saveBoard, loadBoard, resetBoard) required for the main storing and loading of information, to prevent the diagram from being too complex.
Here is a brief overview of how the storage handles the data storage:
Version | As a … | I want to … | So that I can … |
---|---|---|---|
v1.0 | new user | see usage instructions | refer to them when I forget how to use the application |
v1.0 | player | do only valid moves | play chess properly |
v1.0 | player | start a new game | play chess multiple times |
v1.0 | player | see the current state of the chess board on every turn | think about what move to play |
v1.0 | player | tell which symbol represents which piece | know what is where |
v1.0 | player | specify move coordinates | move the piece I want how I want it |
v1.0 | player | promote pieces when the option is available | play extended games properly |
v1.0 | player | have the option to abort the game | leave the game when I no longer want to play |
v1.0 | player | save and get back to a game | leave when I am busy and resume a game when I am free |
v2.0 | new player | see available moves for a piece | learn the rules of chess and valid moves |
v2.0 | new player | refresh the rules of chess anytime | recap and learn the rules of chess |
v2.0 | player | see past move history | recap through the gameplay |
v2.0 | player | see captured pieces | gauge the state of the game |
This section describes the process of manual testing for ChessMaster. ChessMaster utilises JUnit 5.10.0
for automated testing. Please ensure you are using the same version before proceeding.
In addition to unit tests, we have also set up functionality for end-to-end tests. End-to-end testing, in the context of ChessMaster, involves testing the entire program, simulating user interactions, and verifying that the system functions as expected. These tests cover a wide range of scenarios, from setting up the game to making moves and evaluating outcomes.
The general idea is to compare expected output to real output, as usual. But we must first capture the System.out
output to be able to call assertEquals()
on it with the expected output. This is what our class ConsoleCapture
does: it just redirects all output from System.out
to a new, temporary stream. We start the stream before each test, run the game, and then stopCapture()
after the game is complete.
In general, to create your own end-to-end test, follow these steps:
./src/test/java/chessmaster/endtoend
directory. The name of your class should reflect the feature or scenario you plan to test e.g. HistoryTest
tests the History command.ConsoleCapture
class. This class redirects all output from System.out
to a temporary stream, allowing you to capture the output during the test. Begin capturing the output before each test and stop it after the test is complete.ConsoleCapture
to capture the program’s output and compare it to the expected output. The expected output should be saved in a text file under the ./src/test/resources
directory.Some sample stub code is provided below.
public class Test {
@BeforeEach
public void setup() {
// Create temporary storage file just for tests
String filepath = "testingStorage.txt";
File file = new File(filepath);
try {
file.createNewFile();
} catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
}
this.storage = new Storage(filepath);
consoleCapture = new ConsoleCapture();
consoleCapture.startCapture();
}
@AfterEach
public void shutdown() {
String filepath = "testingStorage.txt";
File file = new File(filepath);
file.delete();
}
@Test
public void historyCommand_twoMovesWhiteStarts() {
// Convert user input string to an InputStream and tell Java to use it as the input
String testInput = "<your input here>";
ByteArrayInputStream in = new ByteArrayInputStream(testInput.getBytes());
System.setIn(in);
// Need to create TextUI() after setting System input stream
ui = new TextUI();
// Create a new board and game with your desired testing preferences
board = new ChessBoard();
Game game = new Game();
// Run the game. This will automatically use the `testInput` string as user input
game.run();
// Compare captured output with expected output and assert
consoleCapture.stopCapture();
String capturedOutput = consoleCapture.getCapturedOutput();
String expectedOutput = readExpectedOutputFromFile("src/test/resources/historyCommand_twoMovesWhiteStarts.txt");
assertEquals(expectedOutput, capturedOutput);
}
}
Some notes about the above code:
Things to note when creating your own test:
testInput
MUST end with the abort command (or a win condition) for the test to run properly.By following these steps, you can create end-to-end tests that cover various gameplay scenarios and command usage. These tests help ensure that the program functions correctly and produces the expected output in response to user interactions.