Chess Program Played Through Terminal
by n0ne9999 in Design > Software
17 Views, 0 Favorites, 0 Comments
Chess Program Played Through Terminal
You may have already heard of online chess if you've been on the internet for some time, but have you ever considered the program behind it? On the surface, the rules of chess are relatively simple, but that only extends to human interpretation. A computer doesn't have an easy time managing the rules for a game of chess. A simple check may result in several calculations to make sure a legal move exists. This guide will explain the full implementation of the basic rules of chess in computer language.
Supplies
You will need the following:
- A computer
- Internal Development Environment (IDE) of a programming language of your choice
- Compiler (if applicable)
This guide's chess program is written in Java. The implementation is designed to be easily written in any language. Some basic knowledge of programming is recommended.
Understanding the Game Flow
Before you begin, you must first understand, besides the basic rules of chess, how the game will flow in the program itself. There are many approaches to this, each with their upside and downside. This guide will focus on a simplistic approach meant to keep the implementation straightforward. Here is an overview of the expected flow of the game in the main control function:
- Prompt user input from first player
- Validate move determined by input according to the piece's movement rule
- These rules should not make permanent changes to the board. This is the responsibility of the main control function.
- Check afterwards if the king of the current player is under attack
- Undo if it is
- Additional functionality post-move
- Castling requires an extra rook move on the same move
- Pawns reaching the final rank are promoted
- Check if enemy king is under attack
- Check for legal moves the other player may play to defend the king
- If there is none, the king is mated and the game ends
- Repeat 1 through 5 for the other player, then return to the first player and so forth
I recommend you take some time to fully write this out as pseudocode.
Initial Set Up
In your IDE, open a new file or whatever is needed to write the program. Depending on your language of choice, you may need to include some initial code for your program to enter from. This will usually be included on file creation for most IDE's.
Once the editor is open, you'll first want some initial global/static variables and functions. There are distinctions between global and static variables, but for the purposes of this program, these will function identically.
Variables
- a two-dimensional character array "board"
- constant character representations for each piece type of each color
"board" will represent the 8x8 chess board where the pieces are stored. Thus, both the inner and outer arrays are of length 8. The pieces are represented by a character stored in "board". Therefore, each piece type for each color will need a unique character to represent them.
These variables need to be initialized before the program begins. For clarity, the outer array's indices will represent the rank and the inner array's indices will represent the file. Each element in this two-dimensional array will be referred to as a square.
For character representations, you can simply use the following:
- K = King
- Q = Queen
- R = Rook
- N = Knight
- B = Bishop
- P = Pawn
As for creating separate representations for white and black, you can use uppercase letters for white and lowercase letters for black or vice versa. For clarity, this guide names these variables following the format "[piece type]_[first letter of color]" (e.g. "PAWN_W").
You may use a different set of characters if you have a better method.
Don't forget that empty squares also need to be represented. You should know ahead of time what the character representing an empty space is. Most of the time, it will, by default, be a null character.
You can use literal space characters (i.e. " ") over null characters if you prefer or are unfamiliar with them. This will be especially helpful to remember when your program on output doesn't fill in the null character with whitespace.
Functions
- output function "displayBoard" with one boolean parameter
- bounds checking function "isOutOfBounds" with two integer parameters
- test function "test" with no parameters
"displayBoard" will read the "board" variable and output it to the terminal formatted like a chess board. The boolean parameter will be used to rotate the board. As "board" is a static variable, you do not need it passed into the function and can instead be directly accessed. Below is an example of the output.
"isOutOfBounds" will simply check if the two integers passed to it is within the arrays' bounds. As the length is 8, the indices range from 0 to 7 (may instead be 1 to 8 in some languages).
"test" is your primary function for debugging your other functions. It will be separate from your main game function to test the other functions across various cases to verify if they work as intended. Test cases will be listed after more complex steps. Not all possible edge cases will be listed, only those that are results of probable logical errors. Remember to reset any changed static variables or clear the function after each set.
Movement Rules: Primary Pieces
The first functions you'll need are the movement rule functions. These will take four integer parameters, two for starting position and two for finishing position, and return a boolean value. If true, then the move is valid; the control function can proceed to the next steps.
Function signature example:
The Rook, Bishop, and Queen (Orthogonal Movement)
The easiest of the pieces to implement are the orthogonal-moving and diagonal-moving pieces. These rules can be made into their own functions and called by the dedicated movement rules of the pieces:
- Rook = orthogonal
- Bishop = diagonal
- Queen = orthogonal & diagonal
An orthogonal move moves a piece along either the rank or file of the board. To determine if a move is orthogonal, you can simply compare either starting rank to finishing rank or starting file to finishing file for equivalence, creating a boolean expression.
Here is what it should look like:
Before you return true for both if statements, you have to consider obstructing pieces on the board.
Neither orthogonal-moving nor diagonal-moving pieces can jump over pieces. If there are any in the way, enemy or not, you return false instead.
A simple solution is to loop over the ranks or files in between their respective start and finish rank/file. Here is what the full function should look like now:
You'll also want to deny moves that attempt to "move" in place. These will meet both conditions so to address this, add the second condition within the block for the first and return false.
The Rook, Bishop, and Queen (Diagonal Movement)
Diagonal moves are implemented similarly to orthogonal moves, with some slight differences making the implementation trickier.
A diagonal move moves a piece the same distance in rank and file. You can think of it as the difference between the rank and file remaining the same for both the starting and finishing square. This will allow you to create a boolean expression to determine diagonal moves that move increasing or decreasing in both rank and file.
However, that is half of the problem. The other half are diagonal moves that are rank increase, file decrease or rank decrease, file increase.
Returning to the mathematical point of view, now, the sum remains the same instead of the difference. The increase in rank or file is negated by the decrease in the other, leaving the sum of the two unchanged from start to finish.
Putting all this together, you should get something that looks like the following:
Of course, you'll also need to check for obstruction and as well as preventing "move" in place like you already have with orthogonal moves.
Remember that the loop is different based on which of the two boolean expressions it satisfies, hence the separate blocks. You can see the second one decreases in file instead.
Now that you have the orthogonal and diagonal validation functions. You can now implement the rook, bishop, and queen movement validation functions. For example:
However, keep in mind, this isn't the full implementation and will be built upon in a later step.
The Knight
The movement of the knight differs from the other pieces.
First, they move in a pattern of two squares in one direction, then one in one of the two directions perpendicular to the direction it just moved in. In total, a knight has 8 possible squares it can move to.
When visualized, these squares form two rectangles, one that is longer in rank, the other in file. These arbitrarily formed rectangles can then be combined to create the boolean expressions:
To explain:
- Start with a basic knight move of increase of two in rank, increase of one in file.
- Applying absolute value to the rank allows the expression to accept decrease of two in rank, increase of one in file.
- Repeat for file and now you complete the rank-elongated rectangle.
- Do the same for one in rank, two in file, and you get the other rectangle as described before.
- To get the full movement pattern, take the union of these two expressions.
Now you have your knight movement validation function:
Test Cases
Test the for the following returns with the given arguments:
- orthogonal(0, 0, 0, 1) = true
- orthogonal(0, 0, 1, 0) = true
- orthogonal(0, 0, 1, 1) = false
- orthogonal(0, 0, 0, 0) = false
- diagonal(0, 0, 1, 1) = true
- diagonal(0, 1, 1, 0) = true
- diagonal(0, 0, 0, 1) = false
- diagonal(0, 0, 0, 0) = false
- knight(0, 0, 1, 2) = true
- knight(0, 0, 2, 1) = true
- knight(0, 0, 1, 1) = false
Test the following, like above, with a piece, any piece, placed on the board at "d4" (3 and 3 for array indices):
- orthogonal(0, 3, 6, 3) = false
- orthogonal(3, 0, 3, 6) = false
- diagonal(0, 0, 7, 7) = false
- diagonal(0, 1, 5, 6) = true
- diagonal(6, 0, 1, 5) = false
- diagonal(7, 0, 0, 7) = true
For 1-indexed arrays, add 1 to all arguments, including where to add the pawn)
Not Taking Your Own Pieces
Making sure pieces follow their respective movement rules is good and all, but taking your own pieces is not. You can address this by creating a function, named "canMoveOrCapture" with two character parameters, to compare if the square a piece is moving to is empty or occupied by an enemy piece.
Since there are 6 types of pieces, writing out a boolean expression each time you need to check for a white or black piece is undesirable. Therefore, you'll also want to create both the "isWhite" and "isBlack" helper functions with one character parameter each.
"isWhite" returns the union of boolean expressions comparing the parameter to all characters representing white pieces and "isBlack" returns the same union of boolean expressions but with black pieces.
Now, before you ask, these functions aren't perfect complements to each other. The complement of one is the union between other and null character. This will be helpful in further simplifying the boolean expression when accepting pieces where an empty square is also acceptable.
The "canMoveOrCapture" function can then be implemented by returning one of these two helper functions based on the piece color. Since it is returning whether the square is empty or occupied by an enemy piece, you want to return the complement of the function for the color of the piece moving. Example:
With this, you can take the intersection of a piece's movement rule and this function to prevent capturing your own pieces. Remember that this function takes two characters for parameters. Retrieve them from the "board" variable.
You can also include the "isOutOfBounds" function from before to also prevent accidental out of bound parameters, though you are unlikely to rely on it with proper user input validation later on.
The rook movement rule from before now should look like this:
You can also include a bounds check for starting position too since that can also lead to an accidental exception. Only the check for finishing position was included as there were potential cases when designing the implementation where bounds checking at least for the finishing position would allow other functions to not worry about doing one before calling the actual move validation function.
Test Cases
- Use two rooks of opposing color, placed on the same rank or file and test "canMoveOrCapture" for true for both.
- Once with the white rook as the starting square. Once for the black rook as the starting square.
- Change the color of one rook such that both rooks are the same color and call the function again for false
- Repeat but with both rooks on the other color
Movement Rules: the Pawn
The pawn, despite being the weakest piece prior to promotion, has the second most complex implementation out of all the pieces. The movement rules are as follows:
- Pawns can only advance forward.
- Forward for white pawns is increasing in rank
- Forward for black pawns is decreasing in rank.
- Pawns can only advance one rank forward each turn.
- The exception of the first time they move, where they can advance up to two ranks.
- File remains the same when not capturing.
- Pawns capture diagonally.
- Pawns can only capture pieces on the adjacent files, one rank ahead.
- A special case known as "en passant" allows pawns to capture enemy pawns that attempt to pass capture by utilizing its first move two-rank advancement.
- The pawn can then capture the passing pawn as if it didn't and only advanced one rank.
- Pawns promote upon reaching the furthest rank possible on the board.
- For white pawns, this is the 8th rank
- For black pawns, this is the 1st rank
- They can promote into any one of the four primary pieces.
- The movement rule function will not be responsible for handling this.
Basic Cases
That was a lot of information to take in so begin by starting small, with the easiest part, advancing forward. For simplicity, only focus on the white pawn for now.
- In the function, begin with an if statement checking if the starting square contains a white pawn.
- Nest inside a new if statement that will contain a boolean expression for the difference between the finishing and starting ranks equaling one.
- Add a third if statement inside the second one, this time for the difference between the starting and finishing files equaling zero.
- Lastly, check if the finishing square is empty as a boolean expression and return it.
- A piece directly ahead of the pawn prevents it from advancing.
Afterwards, your code should look something like this:
Next, you want to implement capturing for the pawn, followed by the first move double advancement.
- Capturing:
- Capturing will be able to satisfy up until rank difference equaling one but fail on the condition for file difference equaling zero. Place an else-if statement following the end of the block.
- The condition should be the difference equaling positive or negative one.
- Can be simplified using absolute value.
- You'll also have to check that the square contains a black piece.
- Don't return it directly though, there will be an additional case the follows from this condition's dissatisfaction later on.
- Double advancement:
- Double advance fails at the rank condition. Place another else-if statement following that block.
- The condition will instead be for the difference equaling two instead of one.
- It also needs to be the first move. A simple implementation is to make sure the pawn is on its starting rank.
- 2nd for white.
- 7th for black.
- All the other conditions for non-capture movement also need to be satisfied.
- These can be combined under one complete boolean expression using the AND operator.
- An additional empty square check is needed for the finishing square, likewise, joined with AND.
With these two additional cases, the "pawn" function should now look like the following:
The En Passant Case
Last is the en passant case. Most of the complexity from implementing this case comes from the immediate play on the turn following a two-rank advancement. To solve this, you'll need a new static variable.
This new static variable, named "enpassant", will need to accomplish the following:
- The variable needs to exist beyond the scope of the function, hence its static nature.
- Has a default state for when en passant is not possible.
- This variable immediately resets to the default state upon the next successful move.
- Representable by -1
- (or 0 for languages with 1-indexed arrays)
- Indicates what file en passant may occur on
- Lest you have pawns on the other side of the board making cross-board en passant captures.
- Represents the files as the index of the inner array in "board"
- Range from 0 to 7
- (1 to 8 for languages with 1-indexed arrays)
This variable can then be updated on a pawn move featuring a two-rank advancement and subsequently used on the following turn by the next call to a pawn function to allow a capturing move without the presence of an enemy piece directly on the finishing square.
Setting the value of enpassant:
However, on said subsequent move, the function itself should not reset the variable. This is because it needs to be reset on any and every move, not just on pawn move. Therefore, it should be left to the main control function to reset.
As for the implementation:
- If you recall, the capture rule was made into its own conditional statement rather than returned directly as boolean expression. This case is precisely why it's implemented as such.
- En passant is a unique case where it is the only capture where the captured piece isn't on the finishing square of the captor piece.
- The pawn's capture rule will fail just short of the return because there is no enemy piece on the finishing square.
- You can then use that unsatisfied condition to enter the en passant case.
- Place an else-if statement after the regular capture condition.
- It will instead check for:
- if the finishing file matches the file indicated by the static variable.
- if the rank is the correct rank for en passant
- 5th for white
- 4th for black
- With how the main control function will be implemented, it will not know that the to be captured piece is not on the finishing square based solely on the movement rule's return value. You'll need another static variable, a boolean named "epCommit".
- This time, its purpose is to inform the function that the en passant case was used to validate a move.
- This will be set to true within the block for the en passant case.
- Reset will be handled by the control function.
The implementation should now look something like this:
Rewriting for Black Pawns
The movement rule for black pawns mirrors its white counterparts. Copy the entire block for the white pawn movement rule and paste it after an else statement following the first if statement. Then change the following:
- Second if statement now checks for difference equals -1.
- If statement checking for a black piece on the finishing square now checks for a white piece
- En passant case checks for "rankS == 3" instead of "rankS == 4" (4th rank instead of 5th rank)
- The final else-if statement for two-rank advancement will check for "rankS == 6" instead of "rankS == 1" (7th rank instead of 2nd)
Test Cases
- Setup the board with a white pawn on "e2" and a black pawn on "d7"
- Test "pawn" for true with arguments single and double advancement for both pawns.
- Add to the board:
- White pawn on "e3" and "e5"
- Black pawn on "d6"
- Test the same arguments for #2 for false
- Test if the "e5" pawn can capture the "d6" pawn and vice versa
- Remove pawns at "d7", "d6", "e2", and "e3"
- Add to the board:
- White pawn on "e4"
- Black pawn on "d4" and "d5"
- Set the "enpassant" variable to the "d" file (3).
- Test "pawn" for true with the arguments:
- 5th rank (4)
- "e" file (4)
- 6th rank (5)
- "d" file (3)
- Check if the "epCommit" flag was set to true. Reset it to false afterwards.
- Set the "enpassant" variable to the "e" file (4)
- Test "pawn" for true with the following arguments:
- 4th rank (3)
- "d" file (3)
- 3rd rank (2)
- "e" file (4)
- Check if the "epCommit" flag was set to true.
Determining the Piece
Now with 5 of the 6 pieces' movement rules implemented, you should now write the function to determine which of the movement rules to call.
The king has yet to be implemented yet and will be later. You can't do so yet because of the additional functions needed, this one included. For now, write out the king movement rule function signature, but leave the implementation as a stub. You still want to include it as you're implementing this function.
This function will be named "validateMoveRule". It is a simple function that takes the same four parameters every movement rule takes, passes them to the respective piece's movement rule function, and returns the boolean returned from said function.
All this function's implementation amounts to is:
- Retrieving the character representation of a piece.
- Matching it to a long list of comparisons.
- Once a match is found, call the corresponding movement rule function
- Return the function's return value.
- If no matches exist, return false (square is empty).
The image provided for this step is an example of such an implementation. If you're uncomfortable with switch-case statements, you can always use traditional if-else statements.
Check
Before you can implement the king's movement rule, you must first handle check. As the most valuable piece, a player cannot leave nor put their own king in check. You'll be implementing a dedicated function for this purpose.
Outline
To outline what this function's purpose is:
- Takes two integers, the rank and file of the king, and a boolean and returns a boolean:
- True if the square from the integer parameters is actively under attack
- False if not
- Relies on a piece already present in parameter-located square to call movement rule functions for pieces of the right color.
- The boolean parameter will prevent the updating of new static variables introduced later in this step.
- Used to detect if the king is under attack
- Called by the main control function to determine threat to a piece, to both:
- determine if the enemy king is now in check after a move.
- undo an illegal move due to king safety.
- Called while determining checkmate to see if a would-be safe move to protect the king doesn't leave the king still threatened.
- Called by the king's movement rule during castling as to not castle through check.
This function hasn't been given a name yet. Though calling this function "isCheck" is not bad, it isn't representative of the full use cases. Preferably, call it "underAttack" instead.
Function signature:
Implementation
The implementation is as outlined:
- Loop through all squares on the board
- Call "validateMoveRule" on the current loop iteration's square as the starting square and the king's square as the finishing square.
- Effectively this will attempt to validate a capture of some piece on the king's square for every piece on the board.
- Of course, same color pieces will always fail the validation because a prior step specifically addressed this issue.
- You only need to find the first case where an attack is possible before ending the loop early.
- If a piece is able to attack the king, return true. Return false if otherwise.
Faking the Move
Recall that the pawn movement rule function utilized static variables in order to properly handle en passant. If you were to call it here, you risk modifying them to a state inconsistent to what the main control function has set them to, resulting in a move with incorrect legality.
Modifying the signatures of all 6 movement rule functions to pass an additional boolean would be somewhat undesirable since 4 of the 6 would not utilize it. It is not an invalid approach, but for simplicity, implement it using static variables.
Call this new static boolean variable "noMove". It will act as a flag for "pawn" to skip updating its respective static variables. There are only two places where "pawn" has static variable assignments. Add a single condition for the complement of "noMove" before assigning:
In "underAttack", set this flag to true upon its entry and reset to false before any return.
Remembering the Attacking Piece
In later steps, knowing exactly where a threat to the king is coming from will save a lot of time and computational power. Unfortunately, you won't be able to return this information on top of a boolean without some form of custom encapsulation.
Instead, the solution again is to utilize static variables. These static variables will be named "rCheck" and "fCheck" and they respectively store the rank and file of the attacker found from this function if one is found. If not, they will both be reset to a default state of -1.
Remember that this function may be called by other functions that aren't the main control function, reusing the code that is meant serve as an "isCheck" function, but not for determining check. In such cases, you don't want "rCheck" and "fCheck" updating for some temporary move and there isn't reflective of the actual board state. These variables are what the boolean parameter outlined exists to prevent. Make sure the assignments are gated behind a condition for this parameter.
It is also worth noting that you only need one set of these variables entirely instead of one set for each color. This is because by the time the control function enters the other players turn, check must have been resolved already. Therefore, the same variables are free to be reassigned when needed.
Test Cases
- Setup the board with a white king and black rook on the same rank or file.
- Test for true by passing the function the rank and file of the king, and true for the boolean parameter.
- Check if the "rCheck" and "fCheck" variables were updated to the rank and file of the black rook respectively.
Check But With Extra Steps
The "underAttack" function provides a convenient encapsulation of frequently used operations in resolving an attack on the king. However, if you noticed from the outline, two of the three calls want to perform a temporary move beforehand. Therefore, it is ideal to implement an overloaded version of the function to do just that before actually calling the original function. If your language doesn't allow for function overloading, you can simply write it under a different name.
Function Signature
This function will instead take six parameters and return a boolean, the same one returned by the original function. The first two will directly be passed into the original. The other four will describe the piece movement.
The parameters are as follows.
- current rank of the king
- current file of the king
- starting rank
- starting file
- finishing rank
- finishing file
Implementation and the Return of the Exceptional Case
For implementation, the function only needs to perform a move, call the original function, and undo the move. This is ultimately what the function is responsible for. However, there are certain extra steps that would result in this description being shortsighted.
Though the move may be temporary, the actual board is still receiving updates. If the move involves a capture, you must replace the captured piece on the board when done. This isn't so bad as you can retrieve the character on the finishing square before moving. However, there exists one case where you cannot find the captured piece on the finishing square. If you recall, it's the en passant case.
Therefore, for the en passant case specifically, you will need to retrieve the piece with a different rank from the finishing rank. Every other case will have this rank variable identical to the finishing rank. The file will remain the same in all cases.
Outline below is a simple solution:
- Intialize three variables:
- a character variable for storing the retrieved character representation from "board"
- an integer variable for rank
- an integer variable for file
- can directly be initialized to finishing rank.
- Set "noMove" flag to true. The movement rule functions are once again being utilized outside of the main control.
- Set the rank variables to the following based on the condtions:
- If the pawn is white and en passant is attempted and valid, rank = starting rank
- If the pawn is black and en passant is attempted and valid, rank = starting rank
- For all other cases, rank = finishing rank
- Store to the character variable the character at the square found using the local rank and file variables
- Alternatively, for the pawn cases, you can add an additional comparison for a white/black pawn and directly assign the character representation.
- The other case will then perform the assignment within the third case instead of after the conditional statements.
- Reset "noMove" flag.
- Remove the to be captured piece (if any) by setting the square to 0 for empty space
- Perform the move itself.
- Call "underAttack" with the parameters for the king square.
- The boolean parameter will be false
- Perform another move in the reverse order.
- At the square of the captured piece, set it back to the character representation stored in the local variable.
Example for number 2, 3, 4, and 5:
Moving to a Helper Function
Later, you'll have to perform moves in the main control function as well. To simplify the code slightly, take the two lines necessary to perform a move and replace it with a single line for the function "move" that replicates exactly that:
Test Cases
- Reuse the same setup for the testing of the step before (Step 7).
- Add a white rook such that it can move to block the attack.
- Call "underAttack" with the arguments for the rank and file of the white king, rank and file of the white rook, and rank and file of the square the white rook is to move to to block the black rook's check.
- Test if the return is false.
- Verify that the white rook did not actually move following the return from the function call.
Movement Rules: the King
With the "underAttack" function complete, you can now implement the king's movement rule.
- The king can only move a distance of one in rank, file, or both.
- The king cannot move into check.
- The movement rule function itself can afford to skip this and let the main control function resolve this rule in its place.
- This is because this rule is more of an extension of the general rule of not ending your turn with the king in check, and thus, all pieces need to have this rule in some form.
- The king may castle under the following conditions.
- The king and a (same color) rook have not moved since the beginning of the game.
- There are no pieces between the king and said rook.
- The king currently isn't in check, won't be castling into check, nor passing the square in between while said square would be in check.
- The rook (nor the square only it passes, if applicable) does not need to be considered for the check condition.
- When a king castles, the following occurs:
- If the rook is on the "a" file (queenside):
- The king is moved to the "c" file.
- In the same move, the rook is moved to the "d" file.
- If the rook is on the "h" file (kingside):
- The king is moved to the "g" file.
- In the same move, the rook is moved to the "f" file.
- All moves start and finish on the same rank.
- 1st rank for white.
- 8th rank for black.
Basic Movement
The basic movement is the easiest part of the rule. This can easily be determined with the following boolean expression:
This expression will compare for rank difference and file difference to be less than 1 but not if both are 0. Remember to also include a call to "canMoveOrCapture" to not have captures on friendly pieces.
Castling Rights Introduction
The hard part to handle is castling. You may be tempted to perform the extra rook move within this function, but that would be against the design philosophy the movement rules have been implemented under, that is to only validate a move and leave no permanent alteration of the actual board, if any.
However, castling has a few conditions that need to be managed beyond scope of a single call to this function. Castling rights are removed upon the first move a relevant piece for that specific right. Since pieces can simply return to the home square, the same board state may have different castling rights, in which the movement rule function cannot differentiate without the help of static variables.
Castling Rights Implementation
The variable only needs to be a boolean flag for when its specific castling right is removed. Because there are two rooks for a given color and each rook is tied to its own castling right with the king, there needs to be two flags for each color for a total of 4:
- wKingCastleS
- wKingCastleL
- bKingCastleS
- bKingCastleL
The "S" at the end of the variable name refers to short castling or kingside. Likewise, the "L" refers to long castling or queenside. Initialize all 4 to true. They will be set to false on any move of the associated piece in the main control function.
The movement rule function only needs to read the variables. If a castling attempt is made, the variables will inform the function if the specific right to castle is present.
Input for Castling
Note that the castling move itself will also need to be validated from the same 4 integer parameter input that all movement rules take and will need to be distinguished from the basic movement. Also, castling is a very limited move when it comes to the specific squares it may involve, but that can work to your benefit.
Since all castling starts on the "e" file and end with the king on the "c" or "g" files, the validation can be hardcoded to these specific files, "c" for queenside, "g" for kingside. These files also are not valid for the basic king movement from the "e" file, so no conflict occurs there. In addition, castling always occurs entirely on the home rank so that can also be hardcoded into the condition, 1st for white, 8th for black. This gives you one specific input for each castling right.
Empty Squares for Castling
The check for empty squares is a simple check. Depending on which exact castle is performed, the squares to check for emptiness will differ. For each side of castling for each color, check the following squares
- White kingside
- "f1"
- "g1"
- White queenside
- "b1"
- "c1"
- "d1"
- Black kingside
- "f8"
- "g8"
- Black queenside
- "b8"
- "c8"
- "d8"
Check Yet Again
The final condition that needs to be met are the three squares that need to be checked for check. Like with basic movement, the final square in the move can be omitted from this function as that can be handled by the main control function. This leaves the starting and the intermediate square.
This is where the "underAttack" function(s) created before come into play. This creates an easy and compact means of verifying that the squares in question are free from any attack. Though, before you call it for both, there is an easier method to verify the safety of the king's starting square.
As specified in an earlier step, knowing where an attacker to the king will prove useful. Here is where that static variable from before has a use. If the "rCheck" and "fCheck" static variables are in the default state, -1, no attack on the king is active. Any other state indicates a threat on the king. This can then be placed before the program enters the block for castling to stop any attempts at castling under check.
This leaves the only the intermediate square in need of a call to the "underAttack" function. You need to call the 6-parameter overload with a temporary move of the king onto the intermediate square. The 6 arguments for each parameter can be hardcoded as well with different values for each of the castling variations.
Also worth noting, by now, all previous conditions would have identified which color and side of castling is being attempted. This call can then be directly returned as no further code within the movement rule function need to be considered.
Full Castling Condition in Code
Now to put all the conditions together. Using white kingside castling as an example, each version of castling should have a block of code that looks like this:
You may refer to the source code provided on the final step for the other three sets of hardcoded values.
Test Cases
- Setup the board (and static variables) such that all four castling versions can be performed.
- White king at "e1"
- Black king at "e8"
- Rooks on the four corners, matching the color of the king on the same rank. (Optional)
- Test all four for true.
- Set the "rCheck" and "fCheck" static variables to a non-default state.
- Test any one of the four castling versions for false.
- Reset "rCheck" and "fCheck".
- Setup the board with new pieces such that "d1", "f1", "d8", and "f8" are under attack by enemy pieces. Example setup:
- White rooks at "d7" and "f7"
- Black rooks at "d2" and "f2"
- Test all four castling versions for false.
- Setup the board to the original first test case.
- Set all castling right flags to false.
- Test all four castling versions for false.
Checkmate (Preliminary Step)
With all six movement rules complete, you can now focus on controlling the game itself. But first, you should implement a function for determining if the king is checkmated following check.
Outline
All this function needs to do is check if there is at least one move that resolves the check. Name this function "isCheckmate". This function returns false if it exists, true if not:
A check may be resolved in three ways:
- Moving the king to a safe square
- Blocking the attack
- Removing the attacker
The implementation can then be generalized to if any one of the three cases is possible, the king is not checkmated. With that in mind, the fully expanded outline is as follows:
- Does the king have a safe square to legally move to?
- Call the king movement rule function on each of the 8 squares a king may move to
- Remember to include a bounds check somewhere, either here or inside the movement rule for the king
- For every true return, call "underAttack" with a temporary king move. This will determine if the square is safe. Return false if false on the first time it does, continue if true.
- The "king" function does not call "underAttack" itself for the finishing square, deferring it to main control. This check needs to be replicated here too.
- Can the attacker have its sight on the king blocked?
- Generate an array of rank-and-file pairings, each locating a square on the board. This will contain all squares between the king and attacker.
- For each square, determine if a piece of the same color as the defending king can move to this square.
- Make sure the move doesn't open a new attacker to the king. If it does, then it isn't counted.
- Return false if one exists, continue if not.
- Can the attacker be captured?
- Determine if a piece can capture the attacker.
- Make sure the move doesn't open a new attacker to the king. If it does, then it isn't counted.
- Return false if one exists, continue if not.
- All cases have failed.
- Return true.
Before you jump to implementation, you should implement the following helper functions.
Helper Function: Path Generator
This function, named "path", is responsible for generating the path needed for case 2. It'll take the usual 4 parameters for start and finish square and return an integer array. The array will store the rank and file in pairs. You can opt to keep the array one dimensional, interpreting every two elements as one square, or increases it to two for explicit separation. The provided code will use the former option. If you are using the latter, remember to adjust loops accordingly. Use the following steps to implement this function.
- The only pieces that can have its attack blocked are rook, bishop, and queen. In other words, only the orthogonal and diagonal moving pieces. You can then simply copy the boolean expressions from those two functions into the conditions in this function.
- Instead of looping until the first obstruction is found, loop over the entire distance, adding the rank and file to the return array each time. The loop range will be exactly the same as it is in the original functions.
- You'll likely need to specify a length for the array. For efficiency, the length of the array should only be as long as the number elements it needs to store.
- Since the board is 8 squares in length for all sides, the greatest number of squares in between two pieces is 6.
- Since you need two integers, rank and file, for each square, the initial length should be 12.
- You'll then want to trim the array. To do that:
- Create a local variable to store the number of elements
- Increases each time you add an element to the array
- After the loop ends, create a new array of the size indicated by the variable.
- Copy over all elements.
- Return the trimmed array.
Refer to the first image of two above provided for this step as a reference.
Helper Function: Another modification to "underAttack"
The purpose of the "underAttack" function is to check the entire board if there is at least one piece attacking the designated square. This certainly has proven useful, but for the purposes of this function, there are a few shortcomings:
- Requires a piece on designated square for selecting only white or black pieces.
- Can't itself find a piece that can move to a square, before temporarily moving said piece and verifying if the king is still safe.
A new function is then required to fill in these gaps. Since the purpose is less about determining if the king is under attack and instead to test if a piece can move/capture on a given square to stop the king from being under attack, it will be named "canResolve" instead and return true on the threat to king being negative instead of false. The parameters it'll take are:
- integer for king's rank.
- integer for king's file.
- integer for the rank to move a piece to.
- integer for the file to move a piece to.
- boolean for color of piece: false for white, true for black.
Internally, the implementation does the following:
- Loop through all squares on the board.
- Set the "noMove" flag. You'll be calling some movement rule functions outside of main control.
- May remain set after a loop iteration. Will not affect results.
- If:
- The square contains a piece matching that of the color indicated by the boolean parameter.
- White piece if false.
- Black piece if true.
- The piece isn't the king.
- The piece has a valid move to the square designated by the rank and file parameters.
- Then:
- Reset "noMove" flag.
- Call "underAttack" with:
- king's rank
- king's file
- the rank of the current iteration of the loop
- the file of the current iteration of the loop
- the rank to move the piece to
- the file to move the piece to
- On return of "underAttack":
- Return true if it returns false.
- Continue the loop if it returns true.
- After the loop finishes, reset "noMove" flag and return false.
Refer to the second image of the two above provided for this step as a reference.
Test Cases
- Set the board with the following:
- White king at "e1"
- Black rook at "e4"
- Test "canResolve" for false with the following arguments:
- 1st rank (0)
- "e" file (4)
- 2nd rank (1)
- "e" file (4)
- white (false)
- Add a white rook at "f2"
- Test for true with the same parameters as before.
- Add a black bishop at "h4"
- Test again for false.
- Repeat 1 through 6 with the opposite colors
Checkmate
A quick reminder before you begin the implementation, the following are the methods of resolving a check:
- Moving the king to a safe square
- Blocking the attack
- Removing the attacker
If any are possible, checkmate is negative.
Implementation
The first method is simple. By now, you should and are expected to be able to implement this without further explanation. Just remember to use the "underAttack" function for the temporary move of the king.
The next method you should implement is the third. This step is completely encapsulated in the "canResolve" function. This
You can extrapolate the color boolean from the attacker or king.
The second method is implemented in a similar fashion to the third, but with a list of squares to check as opposed to a single attacker. Loop through the entire array returned by "path" and replicate what you did for method three.
Lastly, a rare edge case may occur that isn't covered by any the methods. En passant rarely may be the last and only possible move to escape checkmate. The implementation will not be explained, but it can be found in the source code provided on the last step. If you opt to skip it, worst case scenario is that an already losing game is lost a few moves earlier.
Test Cases
- Setup the board with the following:
- White king at "e1"
- Black rooks at "a1" and "a2"
- Set "rCheck" and "fCheck" to reflect the square at "a1" ("rCheck" = "fCheck" = 0)
- Test "isCheckmate" for true with the king's rank and file for arguments (0, 4).
- Add a white rook at "c4"
- Test again for false.
- Move the white king to "b1" (ignore legality)
- Test "isCheckmate" for true with the updated square for arguments (0, 1).
- Add a white bishop at "c3"
- Test again for false.
Building the Main Control Function
So far, the main control function has only existed conceptually. Now it's time to actually write the actual code for it. You are free to choose the name of this function (you always could have for every function technically).
Initialization
You'll first need initialize the board with the standard starting position.
Next, you'll need some form of input reading from the terminal. In Java, this will be provided by scanner class in the java.util library.
You'll also want to track the two kings at all times, so include integer variables for each king's rank and file.
Begin with a loop for the game to run under and a variable to indicate what color's turn it is.
Include variables inside the loop for the input starting rank, starting file, finishing rank, finishing file. These will be assigned from user input.
User Input
Within the loop, begin by prompting the player for input. How you prompt the player and what you accept for input is entirely up to you. You may directly ask for the starting square and finishing square to only handle moves, or you may open with a list of control options that allow players to offer draws and resign.
Whatever you choose, you'll still need to parse the input into integers as arguments for your movement rule functions. The squares on the board so far have been referred to using the format "letter of file" concatenated with "rank number" (e.g. "e1"). This is the format that is commonly used in chess, and thus, is a great option for the player to follow.
As for parsing integers from a string representing a square on the board, you'll at least need to first break up the string into two characters, one for rank, one for file. Both will be characters initially. There are a few options for converting the integers, but the easiest and most universal is a conversion through the ASCII values.
The file can be obtained by subtracting the ASCII value of "a" from the input character. If you use upper case letters, do the same but with "A". These values are 97 for "a" and 65 for "A". In code:
If the language you chose use 1-indexed arrays, add 1 after the conversion.
The rank, likewise, can be converted from character to an integer value in the same manner. This time, subtract with the ASCII value for "1", 49.
If the user fails to provide an input that results in an identifiable square, notify the player of the error, restart the loop, and prompt again.
Validating the Move
Next, it's time to actually verify if the move is legal.
- Locally store a copy of the "enpassant" static variable.
- This will be updated on validation but may need to be restored to the previous state if post validation rejects it due to check.
- Check if the piece at the starting square is of the correct color. If not, restart the loop without a change to the turn color variable (prompt the player again for a new input).
- Be sure to notify the player the move wasn't played.
- Call "validateMoveRule" to initiate the movement validation.
Moving the Piece with Respect to the King
Once the move is validated to be legal, excluding the context of check, you can then proceed to commit to moving the piece on the "board" variable.
- Store the character representation on the finishing square.
- Can be empty.
- If not empty, then it is a capture, which you'll need to restore the piece if the capture is rejected because of check.
- Perform the move. The "move" function from before will work perfectly.
- If the move involved the king, update the local two variables tracking the rank and file of the respective king. Use the rank and file of the finishing square.
- Call the "underAttack" function for the king of the color of the current player.
- The argument for the boolean parameter will still be false as this is meant for the rule about not leaving one's own king in check.
- If "underAttack" returns true:
- Undo the move and restore the piece.
- Restore the "enpassant" variable using the local copy
- Reset the "epCommit" flag potentially set to true by the pawn movement rule function.
- Restore the recently updated local king tracking variables using the starting square.
- Make sure it was a king move again before restoring.
- Restart the loop and prompt the player again, informing them of the illegal move.
- If "underAttack" returns false, continue to the next section.
Updating Additional Information
By now, the move is confirmed and is guaranteed to start the other players turn, if the game hasn't ended. Now you can make the majority of permanent changes to the game state.
- En passant, if played, would not have removed the captured pawn in the base move. This is the purpose of the "epCommit" flag.
- You can easily find the pawn to capture using the "enpassant" variable, which is storing the file of capture the move may be played on with the starting rank of the move (captured pawn is adjacent to captor pawn).
- Reset the "epCommit" flag
- If the "enpassant" variable is not in its default state, reset it to default state.
- Remember to distinguish between it updating this turn or was in that state entering this turn. Use the local copy of the variable which did not receive the update this turn if any.
- If a castle is performed, move the rook as well.
- Use the same hardcoded values in the king movement rule function to identify if the move is a castle and which specific castle it is.
- The rook move can also be hardcoded.
- White kingside: "f1"
- White queenside: "d1"
- Black kingside: "f8"
- Black queenside: "d8"
- Set the castling rights flags to false upon the first move of the associated pieces. This can be identified by the starting square of the move performed
- White king at "e1": both short and long castles for white
- Black king at "e8": both short and long castles for black
- White rook at "a1": long castle for white
- White rook at "h1": short castle for white
- Black rook at "a8": long castle for black
- Black rook at "h8": short castle for black
Pawn Promotion
The last rule of the pawn's movement can finally be implemented. All this sub-step amounts to is prompting the player for their choice of piece to promote to before setting the square the pawn was on the target piece.
- Create a loop. You don't want to be restarting the full loop by now in the event you need to prompt the player again due to invalid input.
- Prompt the player to promote the piece
- You can reuse the suggested letters assignments for character representation from the very beginning.
- Hardcode them as opposed to using the variables. It makes sense for the player to keep using a letter easily accessible on the keyboard when the variables themselves potentially use a representation that isn't.
- Read the input and validate.
- Update the "board" variable with the pawn replaced with the desired piece, if input was successfully interpreted.
- Restart from 1 if it wasn't
Check or Checkmate
The final step before turning over the control to the other player, assuming there is anything to control afterwards.
- Call "underAttack" with the rank and file of the king opposing the current color's turn. This is the only time the boolean parameter is passed true.
- If it returns false, skip to the last step of this list.
- If true, Call "isCheckmate" with the same rank and file arguments for "underAttack".
- If true, end the game, declare the winner is the current player and return to exit the function
- Program immediately terminates.
- If false, switch the boolean for which color's turn to the other color (apply complement).
- Loop should restart from the top again without further control.
Concluding the Project
Now that everything is complete, run the main control function. This will also be your primary method of testing the function. Try to reach and play the various moves with special cases. You can also try to replicate games others have played that you have found if you don't want to come up with the moves yourself.
If you can play through a few games without issues, congratulations! You have programmed an entire game of chess playable through the terminal.
If you still have issues, you can refer to the source file this guide was based on for further reference.
The file is uploaded as a plain text document (.txt) as Instructables does not support .java file extensions. You may manually change the extension or copy into your Java editor for text formatting.