10
\$\begingroup\$

For programming practice, I created a chess program in PHP (my most comfortable language).

The program reads a FEN (a string with the location of all the pieces on the board) from the URL, generates a board, then generates all the legal moves for that position. Then when you click the move, it loads the same file but with a new ?fen= in the URL.

The legal moves list code is 100% complete. It handles all special cases, including pawn promotion, en passant, castling, not allowing moves that place/keep your own king in check, etc.

Moves can be made by double clicking on the move, hitting "Make A Move", or drag and dropping a piece on the board.

I am hoping for feedback/advice to help me make the following improvements to the code:

  • PHP Refactoring - Make the PHP code more readable / more organized / use classes better.
  • PHP Performance - Speed up the PHP code. Complex positions with lots of moves are taking 1500ms to render. This speed is acceptable for a browser based game, but would be way too slow if I wanted to turn this code into a chess engine (chess A.I.)

Humble beginnings: Version 1 of the program can be found here.

Website

http://admiraladama.dreamhosters.com/PHPChess

Screenshot

Screenshot

index.php

<?php

$time = microtime();
$time = explode(' ', $time);
$time = $time[1] + $time[0];
$start = $time;

error_reporting(-1);
ini_set('display_errors', 'On');

require_once('ChessGame.php');
require_once('ChessBoard.php');
require_once('ChessPiece.php');
require_once('ChessMove.php');
require_once('ChessSquare.php');
require_once('Dictionary.php');

$game = new ChessGame();

if ( isset($_GET['reset']) ) {
    // Skip this conditional. ChessGame's FEN is the default, new game FEN and doesn't need to be set again.
} elseif ( isset($_GET['move']) ) {
    $game->board->set_fen($_GET['move']);
} elseif ( isset($_GET['fen']) ) {
    $game->board->set_fen($_GET['fen']);
}

$fen = $game->board->get_fen();
$side_to_move = $game->board->get_side_to_move_string();
$who_is_winning = $game->board->get_who_is_winning_string();
$graphical_board_array = $game->board->get_graphical_board();
$legal_moves = $game->get_legal_moves_list($game->board->color_to_move, $game->board);

require_once('view.php');

ChessGame.php

<?php

class ChessGame {
    var $board;

    function __construct($fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') {
        $this->board = new ChessBoard($fen);
    }

    function __clone() {
        $this->board = clone $this->board;

        if ( $this->move_list ) {
            $array = array();
            foreach ( $this->move_list as $key => $move ) {
                array_push($array, clone $move);
            }
            $this->move_list = $array;
        }
    }

    function get_legal_moves_list(
        $color_to_move,
        $board,
        $eliminate_king_in_check_moves = TRUE
    ) {
        $pieces_to_check = $this->get_all_pieces_by_color($color_to_move, $board);

        $moves = array();

        // TODO: Is player to move checkmated?
        // Is enemy checkmated?
        // If so, return NULL since there are no legal moves. The game is over!
        // Else you will get a weird list of legal moves including moves capturing the enemy king.

        foreach ( $pieces_to_check as $key => $piece ) {
            if ( $piece->type == 'pawn' ) {
                if ( $piece->color == 'white' ) {
                    $directions_list = array('north');
                    if ( $piece->on_rank(2) ) {
                        $moves = $this->add_slide_moves_to_moves_list($directions_list, 2, $moves, $piece, $color_to_move, $board);
                    } else {
                        $moves = $this->add_slide_moves_to_moves_list($directions_list, 1, $moves, $piece, $color_to_move, $board);
                    }

                    $directions_list = array('northeast', 'northwest');
                    $moves = $this->add_capture_moves_to_moves_list($directions_list, $moves, $piece, $color_to_move, $board);

                    // en passant
                    $moves = $this->add_en_passant_moves_to_moves_list($piece, $board, $moves);
                } elseif ( $piece->color == 'black' ) {
                    $directions_list = array('south');
                    if ( $piece->on_rank(7) ) {
                        $moves = $this->add_slide_moves_to_moves_list($directions_list, 2, $moves, $piece, $color_to_move, $board);
                    } else {
                        $moves = $this->add_slide_moves_to_moves_list($directions_list, 1, $moves, $piece, $color_to_move, $board);
                    }

                    $directions_list = array('southeast', 'southwest');
                    $moves = $this->add_capture_moves_to_moves_list($directions_list, $moves, $piece, $color_to_move, $board);

                    // en passant
                    $moves = $this->add_en_passant_moves_to_moves_list($piece, $board, $moves);
                }
            } elseif ( $piece->type == 'knight' ) {
                $oclock_list = array(1, 2, 4, 5, 7, 8, 10, 11);

                $moves = $this->add_jump_and_jumpcapture_moves_to_moves_list($oclock_list, $moves, $piece, $color_to_move, $board);
            } elseif ( $piece->type == 'bishop' ) {
                $directions_list = array(
                    'northwest',
                    'northeast',
                    'southwest',
                    'southeast'
                );

                $moves = $this->add_slide_and_slidecapture_moves_to_moves_list($directions_list, 7, $moves, $piece, $color_to_move, $board);
            } elseif ( $piece->type == 'rook' ) {
                $directions_list = array(
                    'north',
                    'south',
                    'east',
                    'west'
                );

                $moves = $this->add_slide_and_slidecapture_moves_to_moves_list($directions_list, 7, $moves, $piece, $color_to_move, $board);
            } elseif ( $piece->type == 'queen' ) {
                $directions_list = array(
                    'north',
                    'south',
                    'east',
                    'west',
                    'northwest',
                    'northeast',
                    'southwest',
                    'southeast'
                );

                $moves = $this->add_slide_and_slidecapture_moves_to_moves_list($directions_list, 7, $moves, $piece, $color_to_move, $board);
            } elseif ( $piece->type == 'king' ) {
                $directions_list = array(
                    'north',
                    'south',
                    'east',
                    'west',
                    'northwest',
                    'northeast',
                    'southwest',
                    'southeast'
                );
                $moves = $this->add_slide_and_slidecapture_moves_to_moves_list($directions_list, 1, $moves, $piece, $color_to_move, $board);

                // Set $king here so castling function can use it later.
                $king = $piece;
            }
        }

        if ( $moves === array() ) {
            $moves = NULL;
        }

        if ( $eliminate_king_in_check_moves ) {
            $enemy_color = $this->invert_color($color_to_move);

            // Eliminate king in check moves
            $new_moves = array();
            if ( ! $king ) {
                throw new Exception('ChessGame Class - Invalid FEN - One of the kings is missing');
            }
            foreach ( $moves as $key => $move ) {
                $friendly_king_square = $move->board->get_king_square($color_to_move);

                $squares_attacked_by_enemy = $this->get_squares_attacked_by_this_color($enemy_color, $move->board);

                if ( ! in_array($friendly_king_square->alphanumeric, $squares_attacked_by_enemy) ) {
                    array_push($new_moves, $move);
                }
            }

            $moves = $new_moves;

            // TODO: Move notation - disambiguate vague starting squares
            // foreach $moves as $key => $move
                // if $move->piece->type == queen, rook, knight, bishop
                    // make list of destination squares
                    // identify duplicates
                    // foreach duplicates as $key => $move2
                        // if column disambiguate this piece
                            // $move->set_disambiguation(column);
                        // elseif row disambiguates this piece
                            // $move->set_disambiguation(row);
                        // else
                            // $move->set_disambiguation(columnrow);

            // Castling
            // (castling does its own "king in check" checks so we can put this code after the "king in check" code)
            $squares_attacked_by_enemy = $this->get_squares_attacked_by_this_color($enemy_color, $board);
            $moves = $this->add_castling_moves_to_moves_list($moves, $king, $squares_attacked_by_enemy, $board);

            // if move puts enemy king in check, tell the $move object so it can add a + to the notation
            foreach ( $moves as $key => $move ) {
                $enemy_king_square = $move->board->get_king_square($enemy_color);

                $squares_attacked_by_moving_side = $this->get_squares_attacked_by_this_color($color_to_move, $move->board);

                if ( in_array($enemy_king_square->alphanumeric, $squares_attacked_by_moving_side) ) {
                    $move->set_enemy_king_in_check(TRUE);
                }
            }

            // TODO: alphabetize
        }

        return $moves;
    }

    function add_slide_and_slidecapture_moves_to_moves_list($directions_list, $spaces, $moves, $piece, $color_to_move, $board) {
        foreach ( $directions_list as $key => $direction ) {
            // $spaces should be 1 for king, 1 or 2 for pawns, 7 for all other sliding pieces
            // 7 is the max # of squares you can slide on a chessboard

            $xy = array(
                'north' => array(0,1),
                'south' => array(0,-1),
                'east' => array(1,0),
                'west' => array(-1,0),
                'northeast' => array(1,1),
                'northwest' => array(-1,1),
                'southeast' => array(1,-1),
                'southwest' => array(-1,-1)
            );

            // XY coordinates and rank/file are different. Need to convert.
            $xy = $this->convert_from_xy_to_rankfile($xy);

            $legal_move_list = array();

            for ( $i = 1; $i <= $spaces; $i++ ) {
                $current_xy = $xy[$direction];
                $current_xy[0] *= $i;
                $current_xy[1] *= $i;

                $ending_square = $this->square_exists_and_not_occupied_by_friendly_piece(
                    $piece->square,
                    $current_xy[0],
                    $current_xy[1],
                    $color_to_move,
                    $board
                );

                if ( $ending_square ) {
                    $capture = FALSE;

                    if ( is_a($board->board[$ending_square->rank][$ending_square->file], 'ChessPiece') ) {
                        if ( $board->board[$ending_square->rank][$ending_square->file]->color != $color_to_move ) {
                            $capture = TRUE;
                        }
                    }

                    array_push($legal_move_list, new ChessMove(
                        $piece->square,
                        $ending_square,
                        $piece->color,
                        $piece->type,
                        $capture,
                        $board
                    ));

                    if ( $capture ) {
                        // stop sliding
                        break;
                    } else {
                        // empty square
                        // continue sliding
                        continue;
                    }
                } else {
                    // square does not exist, or square occupied by friendly piece
                    // stop sliding
                    break;
                }
            }

            if ( $legal_move_list === array() ) {
                $legal_move_list = NULL;
            }

            if ( $legal_move_list ) {
                foreach ( $legal_move_list as $key2 => $value2 ) {
                    array_push($moves, $value2);
                }
            }
        }

        return $moves;
    }

    function add_capture_moves_to_moves_list($directions_list, $moves, $piece, $color_to_move, $board) {
        foreach ( $directions_list as $key => $direction ) {
            $xy = array(
                'north' => array(0,1),
                'south' => array(0,-1),
                'east' => array(1,0),
                'west' => array(-1,0),
                'northeast' => array(1,1),
                'northwest' => array(-1,1),
                'southeast' => array(1,-1),
                'southwest' => array(-1,-1)
            );

            // XY coordinates and rank/file are different. Need to convert.
            $xy = $this->convert_from_xy_to_rankfile($xy);

            $legal_move_list = array();

            $current_xy = $xy[$direction];

            $ending_square = $this->square_exists_and_not_occupied_by_friendly_piece(
                $piece->square,
                $current_xy[0],
                $current_xy[1],
                $color_to_move,
                $board
            );

            if ( $ending_square ) {
                $capture = FALSE;

                if ( is_a($board->board[$ending_square->rank][$ending_square->file], 'ChessPiece') ) {
                    if ( $board->board[$ending_square->rank][$ending_square->file]->color != $color_to_move ) {
                        $capture = TRUE;
                    }
                }

                if ( $capture ) {
                    $move = new ChessMove(
                        $piece->square,
                        $ending_square,
                        $piece->color,
                        $piece->type,
                        $capture,
                        $board
                    );

                    // pawn promotion
                    $white_pawn_capturing_on_rank_8 = $piece->type == "pawn" && $ending_square->rank == 8 && $piece->color == "white";
                    $black_pawn_capturing_on_rank_1 = $piece->type == "pawn" && $ending_square->rank == 1 && $piece->color == "black";
                    if (
                        $white_pawn_capturing_on_rank_8 || $black_pawn_capturing_on_rank_1
                    ) {
                        $promotion_pieces = array(
                            'queen',
                            'rook',
                            'bishop',
                            'knight'
                        );

                        foreach ( $promotion_pieces as $key => $type ) {
                            $move2 = clone $move;
                            $move2->set_promotion_piece($type);
                            array_push($legal_move_list, $move2);
                        }
                    } else {
                        array_push($legal_move_list, $move);
                    }
                }
            }

            if ( $legal_move_list === array() ) {
                $legal_move_list = NULL;
            }

            if ( $legal_move_list ) {
                foreach ( $legal_move_list as $key2 => $value2 ) {
                    array_push($moves, $value2);
                }
            }
        }

        return $moves;
    }

    function add_slide_moves_to_moves_list($directions_list, $spaces, $moves, $piece, $color_to_move, $board) {
        foreach ( $directions_list as $key => $direction ) {
            // $spaces should be 1 for king, 1 or 2 for pawns, 7 for all other sliding pieces
            // 7 is the max # of squares you can slide on a chessboard

            $xy = array(
                'north' => array(0,1),
                'south' => array(0,-1),
                'east' => array(1,0),
                'west' => array(-1,0),
                'northeast' => array(1,1),
                'northwest' => array(-1,1),
                'southeast' => array(1,-1),
                'southwest' => array(-1,-1)
            );

            // XY coordinates and rank/file are different. Need to convert.
            $xy = $this->convert_from_xy_to_rankfile($xy);

            $legal_move_list = array();

            for ( $i = 1; $i <= $spaces; $i++ ) {
                $current_xy = $xy[$direction];
                $current_xy[0] *= $i;
                $current_xy[1] *= $i;

                $ending_square = $this->square_exists_and_not_occupied_by_friendly_piece(
                    $piece->square,
                    $current_xy[0],
                    $current_xy[1],
                    $color_to_move,
                    $board
                );

                if ( $ending_square ) {
                    $capture = FALSE;

                    if ( is_a($board->board[$ending_square->rank][$ending_square->file], 'ChessPiece') ) {
                        if ( $board->board[$ending_square->rank][$ending_square->file]->color != $color_to_move ) {
                            $capture = TRUE;
                        }
                    }

                    if ( $capture ) {
                        // enemy piece in square
                        // stop sliding
                        break;
                    } else {
                        $new_move = new ChessMove(
                            $piece->square,
                            $ending_square,
                            $piece->color,
                            $piece->type,
                            $capture,
                            $board
                        );

                        // en passant target square
                        if (
                            $piece->type == 'pawn' &&
                            $i == 2
                        ) {
                            $en_passant_xy = $xy[$direction];
                            $en_passant_xy[0] *= 1;
                            $en_passant_xy[1] *= 1;

                            $en_passant_target_square = $this->square_exists_and_not_occupied_by_friendly_piece(
                                $piece->square,
                                $en_passant_xy[0],
                                $en_passant_xy[1],
                                $color_to_move,
                                $board
                            );

                            $new_move->board->set_en_passant_target_square($en_passant_target_square);
                        }

                        // pawn promotion
                        $white_pawn_moving_to_rank_8 = $piece->type == "pawn" && $ending_square->rank == 8 && $piece->color == "white";
                        $black_pawn_moving_to_rank_1 = $piece->type == "pawn" && $ending_square->rank == 1 && $piece->color == "black";
                        if (
                            $white_pawn_moving_to_rank_8 || $black_pawn_moving_to_rank_1
                        ) {
                            $promotion_pieces = array(
                                'queen',
                                'rook',
                                'bishop',
                                'knight'
                            );

                            foreach ( $promotion_pieces as $key => $type ) {
                                $move2 = clone $new_move;
                                $move2->set_promotion_piece($type);
                                array_push($legal_move_list, $move2);
                            }
                        } else {
                            array_push($legal_move_list, $new_move);
                        }

                        // empty square
                        // continue sliding
                        continue;
                    }
                } else {
                    // square does not exist, or square occupied by friendly piece
                    // stop sliding
                    break;
                }
            }

            if ( $legal_move_list === array() ) {
                $legal_move_list = NULL;
            }

            if ( $legal_move_list ) {
                foreach ( $legal_move_list as $key2 => $value2 ) {
                    array_push($moves, $value2);
                }
            }
        }

        return $moves;
    }

    function add_jump_and_jumpcapture_moves_to_moves_list($oclock_list, $moves, $piece, $color_to_move, $board) {
        foreach ( $oclock_list as $key => $oclock ) {
            $xy = array(
                1 => array(1,2),
                2 => array(2,1),
                4 => array(2,-1),
                5 => array(1,-2),
                7 => array(-1,-2),
                8 => array(-2,-1),
                10 => array(-2,1),
                11 => array(-1,2)
            );

            // XY coordinates and rank/file are different. Need to convert.
            $xy = $this->convert_from_xy_to_rankfile($xy);

            $ending_square = $this->square_exists_and_not_occupied_by_friendly_piece(
                $piece->square,
                $xy[$oclock][0],
                $xy[$oclock][1],
                $color_to_move,
                $board
            );

            $legal_move_list = array();

            if ( $ending_square ) {
                $capture = FALSE;

                if ( is_a($board->board[$ending_square->rank][$ending_square->file], 'ChessPiece') ) {
                    // enemy piece
                    if ( $board->board[$ending_square->rank][$ending_square->file]->color != $color_to_move ) {
                        $capture = TRUE;
                    }
                }

                array_push($legal_move_list, new ChessMove(
                    $piece->square,
                    $ending_square,
                    $piece->color,
                    $piece->type,
                    $capture,
                    $board
                ));
            }

            if ( $legal_move_list === array() ) {
                $legal_move_list = NULL;
            }

            if ( $legal_move_list ) {
                foreach ( $legal_move_list as $key2 => $value2 ) {
                    array_push($moves, $value2);
                }
            }
        }

        return $moves;
    }

    function add_castling_moves_to_moves_list($moves, $piece, $squares_attacked_by_enemy, $board) {
        $scenarios = array (
            array(
                'boolean_to_check' => 'white_can_castle_kingside',
                'color_to_move' => 'white',
                'rook_start_square' => new ChessSquare('h1'),
                'king_end_square' => new ChessSquare('g1'),
                'cannot_be_attacked' => array(
                    new ChessSquare('e1'),
                    new ChessSquare('f1'),
                    new ChessSquare('g1')
                ),
                'cannot_be_occupied' => array(
                    new ChessSquare('f1'),
                    new ChessSquare('g1')
                )
            ),
            array(
                'boolean_to_check' => 'white_can_castle_queenside',
                'color_to_move' => 'white',
                'rook_start_square' => new ChessSquare('a1'),
                'king_end_square' => new ChessSquare('c1'),
                'cannot_be_attacked' => array(
                    new ChessSquare('e1'),
                    new ChessSquare('d1'),
                    new ChessSquare('c1')
                ),
                'cannot_be_occupied' => array(
                    new ChessSquare('d1'),
                    new ChessSquare('c1'),
                    new ChessSquare('b1')
                )
            ),
            array(
                'boolean_to_check' => 'black_can_castle_kingside',
                'color_to_move' => 'black',
                'rook_start_square' => new ChessSquare('h8'),
                'king_end_square' => new ChessSquare('g8'),
                'cannot_be_attacked' => array(
                    new ChessSquare('e8'),
                    new ChessSquare('f8'),
                    new ChessSquare('g8')
                ),
                'cannot_be_occupied' => array(
                    new ChessSquare('f8'),
                    new ChessSquare('g8')
                )
            ),
            array(
                'boolean_to_check' => 'black_can_castle_queenside',
                'color_to_move' => 'black',
                'rook_start_square' => new ChessSquare('a8'),
                'king_end_square' => new ChessSquare('c8'),
                'cannot_be_attacked' => array(
                    new ChessSquare('e8'),
                    new ChessSquare('d8'),
                    new ChessSquare('c8')
                ),
                'cannot_be_occupied' => array(
                    new ChessSquare('d8'),
                    new ChessSquare('c8'),
                    new ChessSquare('b8')
                )
            ),
        );

        $legal_move_list = array();

        foreach ( $scenarios as $key => $value ) {
            // only check castling for current color_to_move
            if ( $value['color_to_move'] != $board->color_to_move ) {
                continue;
            }

            // make sure the FEN has castling permissions
            $boolean_to_check = $value['boolean_to_check'];
            if ( ! $board->castling[$boolean_to_check] ) {
                continue;
            }

            // check all cannot_be_attacked squares
            foreach ( $value['cannot_be_attacked'] as $key2 => $square_to_check ) {
                if ( in_array($square_to_check->alphanumeric, $squares_attacked_by_enemy) ) {
                    continue 2;
                }
            }

            // check all cannot_be_occupied_squares
            foreach ( $value['cannot_be_occupied'] as $key2 => $square_to_check ) {
                if ( $board->square_is_occupied($square_to_check) ) {
                    continue 2;
                }
            }

            // Make sure the rook is still there. This case should only occur in damaged FENs. If the rook isn't there, throw an invalid FEN exception (to prevent a clone error later on).
            $rook_start_square = $value['rook_start_square'];
            $rank = $rook_start_square->rank;
            $file = $rook_start_square->file;
            $piece_to_check = $board->board[$rank][$file];
            if ( ! $piece_to_check ) {
                throw new Exception('ChessGame Class - Invalid FEN - Castling permissions set to TRUE but rook is missing');
            }
            if (
                $piece_to_check->type != 'rook' ||
                $piece_to_check->color != $board->color_to_move
            ) {
                throw new Exception('ChessGame Class - Invalid FEN - Castling permissions set to TRUE but rook is missing');
            }

            // The ChessMove class handles displaying castling notation, taking castling privileges out of the FEN, and moving the rook into the right place on the board. No need to do anything extra here.
            array_push($legal_move_list, new ChessMove(
                $piece->square,
                $value['king_end_square'],
                $piece->color,
                $piece->type,
                FALSE,
                $board
            ));
        }

        if ( $legal_move_list === array() ) {
            $legal_move_list = NULL;
        }

        if ( $legal_move_list ) {
            foreach ( $legal_move_list as $key2 => $value2 ) {
                array_push($moves, $value2);
            }
        }

        return $moves;
    }

    function add_en_passant_moves_to_moves_list($piece, $board, $moves) {
        if ( $piece->color == 'white' ) {
            $capture_directions_from_starting_square = array('northeast', 'northwest');
            $enemy_pawn_direction_from_ending_square = array('south');
            $en_passant_rank = 5;
        } elseif ( $piece->color == 'black' ) {
            $capture_directions_from_starting_square = array('southeast', 'southwest');
            $enemy_pawn_direction_from_ending_square = array('north');
            $en_passant_rank = 4;
        }

        if ( $piece->on_rank($en_passant_rank) && $board->en_passant_target_square ) {
            $squares_to_check = $this->get_squares_in_these_directions($piece->square, $capture_directions_from_starting_square, 1);
            foreach ( $squares_to_check as $key => $square ) {
                if ( $square->alphanumeric == $board->en_passant_target_square->alphanumeric ) {
                    $move = new ChessMove(
                        $piece->square,
                        $square,
                        $piece->color,
                        $piece->type,
                        TRUE,
                        $board
                    );
                    $move->set_en_passant(TRUE);
                    $enemy_pawn_square = $this->get_squares_in_these_directions($square, $enemy_pawn_direction_from_ending_square, 1);
                    $move->board->remove_piece_from_square($enemy_pawn_square[0]);
                    array_push($moves, $move);
                }
            }
        }

        return $moves;
    }

    function convert_from_xy_to_rankfile($xy) {
        // XY coordinates and rank/file are different. Need to convert.
        // We basically need to flip X and Y to fix it.

        foreach ( $xy as $key => $value ) {
            $xy[$key] = array($value[1], $value[0]);
        }

        return $xy;
    }

    function get_all_pieces_by_color($color_to_move, $board) {
        $list_of_pieces = array();

        for ( $i = 1; $i <= 8; $i++ ) {
            for ( $j = 1; $j <=8; $j++ ) {
                $piece = $board->board[$i][$j];

                if ( $piece ) {
                    if ( $piece->color == $color_to_move ) {
                        array_push($list_of_pieces, $piece);
                    }
                }
            }
        }

        if ( $list_of_pieces === array() ) {
            $list_of_pieces = NULL;
        }

        return $list_of_pieces;
    }

    // positive X = east, negative X = west, positive Y = north, negative Y = south
    function square_exists_and_not_occupied_by_friendly_piece($starting_square, $x_delta, $y_delta, $color_to_move, $board) {
        $rank = $starting_square->rank + $x_delta;
        $file = $starting_square->file + $y_delta;

        $ending_square = $this->try_to_make_square_using_rank_and_file_num($rank, $file);

        // Ending square is off the board
        if ( ! $ending_square ) {
            return FALSE;
        }

        // Ending square contains a friendly piece
        if ( is_a($board->board[$rank][$file], 'ChessPiece') ) {
            if ( $board->board[$rank][$file]->color == $color_to_move ) {
                return FALSE;
            }
        }

        return $ending_square;
    }

    function try_to_make_square_using_rank_and_file_num($rank, $file) {
        $file_letters = new Dictionary(array(
            1 => 'a',
            2 => 'b',
            3 => 'c',
            4 => 'd',
            5 => 'e',
            6 => 'f',
            7 => 'g',
            8 => 'h'
        ));

        $alphanumeric = $file_letters->check_dictionary($file) . $rank;

        $valid_squares = array(
            'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8',
            'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8',
            'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8',
            'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8',
            'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8',
            'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8',
            'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g7', 'g8',
            'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8'
        );


        if ( in_array($alphanumeric, $valid_squares) ) {
            return new ChessSquare($alphanumeric);
        } else {
            return FALSE;
        }
    }

    function invert_color($color) {
        if ( $color == 'white' ) {
            return 'black';
        } else {
            return 'white';
        }
    }

    function get_squares_attacked_by_this_color($color, $board) {
        $legal_moves_for_opponent = $this->get_legal_moves_list($color, $board, FALSE);

        $squares_attacked = array();
        foreach ( $legal_moves_for_opponent as $key => $move ) {
            // avoid duplicates
            if ( ! in_array($move->ending_square->alphanumeric, $squares_attacked) ) {
                array_push($squares_attacked, $move->ending_square->alphanumeric);
            }
        }

        return $squares_attacked;
    }

    // Used to generate en passant squares.
    function get_squares_in_these_directions($starting_square, $directions_list, $spaces) {
        $list_of_squares = array();

        foreach ( $directions_list as $key => $direction ) {
            // $spaces should be 1 for king, 1 or 2 for pawns, 7 for all other sliding pieces
            // 7 is the max # of squares you can slide on a chessboard

            $xy = array(
                'north' => array(0,1),
                'south' => array(0,-1),
                'east' => array(1,0),
                'west' => array(-1,0),
                'northeast' => array(1,1),
                'northwest' => array(-1,1),
                'southeast' => array(1,-1),
                'southwest' => array(-1,-1)
            );

            // XY coordinates and rank/file are different. Need to convert.
            $xy = $this->convert_from_xy_to_rankfile($xy);

            $current_xy = $xy[$direction];
            $current_xy[0] =  $current_xy[0] * $spaces + $starting_square->rank;
            $current_xy[1] =  $current_xy[1] * $spaces + $starting_square->file;

            $square = $this->try_to_make_square_using_rank_and_file_num($current_xy[0], $current_xy[1]);

            if ( $square ) {
                array_push($list_of_squares, $square);
            }
        }

        if ( $list_of_squares === array() ) {
            $list_of_squares = NULL;
        }

        return $list_of_squares;
    }}

ChessMove.php

<?php

class ChessMove {
    const PIECE_LETTERS = array(
        'p' => 'pawn',
        'n' => 'knight',
        'b' => 'bishop',
        'r' => 'rook',
        'q' => 'queen',
        'k' => 'king'
    );

    var $starting_square;
    var $ending_square;
    var $color;
    var $piece_type;
    var $capture;
    var $check;
    var $checkmate;
    var $promotion_piece_type;
    var $en_passant_capture;
    var $disambiguation;

    var $notation;
    var $coordiante_notation;

    var $board;

    function __construct(
        $starting_square,
        $ending_square,
        $color,
        $piece_type,
        $capture,
        $old_board
    ) {
        $this->starting_square = $starting_square;
        $this->ending_square = $ending_square;
        $this->color = $color;
        $this->piece_type = $piece_type;
        $this->capture = $capture;

        // These cases are rare. The data is passed in via set functions instead of in the constructor.
        $this->disambiguation = '';
        $this->promotion_piece_type = NULL;
        $this->en_passant = FALSE;
        $this->check = FALSE;
        $this->checkmate = FALSE;

        $this->notation = $this->get_notation();
        $this->coordinate_notation = $this->starting_square->alphanumeric . $this->ending_square->alphanumeric;

        $this->board = clone $old_board;
        $this->board->make_move($starting_square, $ending_square);

        // if the king or rook moves, update the FEN to take away castling privileges
        if ( $this->color == 'black' ) {
            if (
                $this->piece_type == 'king' &&
                $this->starting_square->alphanumeric == 'e8'
            ) {
                $this->board->set_castling('black_can_castle_kingside', FALSE);
                $this->board->set_castling('black_can_castle_queenside', FALSE);
            } elseif (
                $this->piece_type == 'rook' &&
                $this->starting_square->alphanumeric == 'a8'
            ) {
                $this->board->set_castling('black_can_castle_queenside', FALSE);
            } elseif (
                $this->piece_type == 'rook' &&
                $this->starting_square->alphanumeric == 'h8'
            ) {
                $this->board->set_castling('black_can_castle_kingside', FALSE);
            }
        } elseif ( $this->color == 'white' ) {
            if (
                $this->piece_type == 'king' &&
                $this->starting_square->alphanumeric == 'e1'
            ) {
                $this->board->set_castling('white_can_castle_kingside', FALSE);
                $this->board->set_castling('white_can_castle_queenside', FALSE);
            } elseif (
                $this->piece_type == 'rook' &&
                $this->starting_square->alphanumeric == 'a1'
            ) {
                $this->board->set_castling('white_can_castle_queenside', FALSE);
            } elseif (
                $this->piece_type == 'rook' &&
                $this->starting_square->alphanumeric == 'h1'
            ) {
                $this->board->set_castling('white_can_castle_kingside', FALSE);
            }
        }

        // if castling, move the rook into the right place
        if ( $this->color == 'black' ) {
            if (
                $this->piece_type == 'king' &&
                $this->starting_square->alphanumeric == 'e8' &&
                $this->ending_square->alphanumeric == 'g8'
            ) {
                $starting_square = new ChessSquare('h8');
                $ending_square = new ChessSquare('f8');
                $this->board->make_additional_move_on_same_turn($starting_square, $ending_square);
            } elseif (
                $this->piece_type == 'king' &&
                $this->starting_square->alphanumeric == 'e8' &&
                $this->ending_square->alphanumeric == 'c8'
            ) {
                $starting_square = new ChessSquare('a8');
                $ending_square = new ChessSquare('d8');
                $this->board->make_additional_move_on_same_turn($starting_square, $ending_square);
            }
        } elseif ( $this->color == 'white' ) {
            if (
                $this->piece_type == 'king' &&
                $this->starting_square->alphanumeric == 'e1' &&
                $this->ending_square->alphanumeric == 'g1'
            ) {
                $starting_square = new ChessSquare('h1');
                $ending_square = new ChessSquare('f1');
                $this->board->make_additional_move_on_same_turn($starting_square, $ending_square);
            } elseif (
                $this->piece_type == 'king' &&
                $this->starting_square->alphanumeric == 'e1' &&
                $this->ending_square->alphanumeric == 'c1'
            ) {
                $starting_square = new ChessSquare('a1');
                $ending_square = new ChessSquare('d1');
                $this->board->make_additional_move_on_same_turn($starting_square, $ending_square);
            }
        }
    }

    // Do a deep clone. Needed for pawn promotion.
    function __clone() {
        $this->starting_square = clone $this->starting_square;
        $this->ending_square = clone $this->ending_square;
        $this->board = clone $this->board;
    }

    function set_promotion_piece($piece_type) {
        // update the piece
        $rank = $this->ending_square->rank;
        $file = $this->ending_square->file;
        $this->board->board[$rank][$file]->type = $piece_type;
        $this->board->update_fen();

        // Automatically pick queen when drag and dropping.
        if ( $piece_type != "queen" ) {
            $this->coordinate_notation = "";
        }

        // update the notation
        $this->promotion_piece_type = $piece_type;
        $this->notation = $this->get_notation();
    }

    function set_enemy_king_in_check($boolean) {
        $this->check = $boolean;

        $this->notation = $this->get_notation();
    }

    function set_checkmate($boolean) {
        $this->checkmate = $boolean;

        $this->notation = $this->get_notation();
    }

    function set_en_passant($boolean) {
        $this->en_passant = $boolean;

        $this->notation = $this->get_notation();
    }

    function set_disambiguation($string) {
        $this->disambiguation = $string;

        $this->notation = $this->get_notation();
    }

    function get_notation() {
        $string = '';

        if (
            $this->starting_square->alphanumeric == 'e8' &&
            $this->ending_square->alphanumeric == 'g8' &&
            $this->piece_type == 'king' &&
            $this->color = 'black'
        ) {
            $string .= 'O-O';
        } elseif (
            $this->starting_square->alphanumeric == 'e1' &&
            $this->ending_square->alphanumeric == 'g1' &&
            $this->piece_type == 'king' &&
            $this->color = 'white'
        ) {
            $string .= 'O-O';
        } elseif (
            $this->starting_square->alphanumeric == 'e8' &&
            $this->ending_square->alphanumeric == 'c8' &&
            $this->piece_type == 'king' &&
            $this->color = 'black'
        ) {
            $string .= 'O-O-O';
        } elseif (
            $this->starting_square->alphanumeric == 'e1' &&
            $this->ending_square->alphanumeric == 'c1' &&
            $this->piece_type == 'king' &&
            $this->color = 'white'
        ) {
            $string .= 'O-O-O';
        } else {
            // type of piece
            if ( $this->piece_type == 'pawn' && $this->capture ) {
                $string .= substr($this->starting_square->alphanumeric, 0, 1);
            } elseif ( $this->piece_type != 'pawn' ) {
                $string .= strtoupper(array_search(
                    $this->piece_type,
                    self::PIECE_LETTERS
                ));
            }

            // disambiguation rank/file/square
            $string .= $this->disambiguation;

            // capture?
            if ( $this->capture ) {
                $string .= 'x';
            }

            // destination square
            $string .= $this->ending_square->alphanumeric;

            // en passant
            if ( $this->en_passant ) {
                $string .= 'e.p.';
            }

            // pawn promotion
            if ( $this->promotion_piece_type == 'queen' ) {
                $string .= '=Q';
            } elseif ( $this->promotion_piece_type == 'rook' ) {
                $string .= '=R';
            } elseif ( $this->promotion_piece_type == 'bishop' ) {
                $string .= '=B';
            } elseif ( $this->promotion_piece_type == 'knight' ) {
                $string .= '=N';
            }
        }

        // check or checkmate
        if ( $this->checkmate ) {
            $string .= '#';
        } elseif ( $this->check ) {
            $string .= '+';
        }

        return $string;
    }
}

ChessBoard.php

<?php

class ChessBoard {
    const PIECE_LETTERS = array(
        'p' => 'pawn',
        'n' => 'knight',
        'b' => 'bishop',
        'r' => 'rook',
        'q' => 'queen',
        'k' => 'king'
    );

    var $board = array(); // $board[y][x], or in this case, $board[rank][file]
    var $color_to_move;
    var $castling = array(); // format is array('white_can_castle_kingside' => TRUE, etc.)
    var $en_passant_target_square = NULL;
    var $halfmove_clock;
    var $fullmove_number;

    var $fen;

    function __construct($fen) {
        $this->set_fen($fen);
        $this->fen = $fen;
    }

    function __clone() {
        if ( $this->board ) {
            for ( $rank = 1; $rank <= 8; $rank++ ) {
                for ( $file = 1; $file <= 8; $file++ ) {
                    if ( $this->board[$rank][$file] ) {
                        $this->board[$rank][$file] = clone $this->board[$rank][$file];
                    }
                }
            }
        }
    }

    function set_fen($fen) {
        $fen = trim($fen);

        // set everything back to default
        $legal_moves = array();
        $checkmate = FALSE;
        $stalemate = FALSE;
        $move_list = array();
        // TODO: add more

        // Basic format check. This won't catch everything, but it will catch a lot of stuff.
        // TODO: Make this stricter so that it catches everything.
        $valid_fen = preg_match('/^([rnbqkpRNBQKP12345678]{1,8})\/([rnbqkpRNBQKP12345678]{1,8})\/([rnbqkpRNBQKP12345678]{1,8})\/([rnbqkpRNBQKP12345678]{1,8})\/([rnbqkpRNBQKP12345678]{1,8})\/([rnbqkpRNBQKP12345678]{1,8})\/([rnbqkpRNBQKP12345678]{1,8})\/([rnbqkpRNBQKP12345678]{1,8}) ([bw]{1}) ([-KQkq]{1,4}) ([a-h1-8-]{1,2}) (\d{1,2}) (\d{1,4})$/', $fen, $matches);

        if ( ! $valid_fen ) {
            throw new Exception('ChessBoard Class - Invalid FEN');
        }

        // ******* CREATE PIECES AND ASSIGN THEM TO SQUARES *******

        // Set all board squares to NULL. That way we don't have to blank them in the loop below. We can just overwrite the NULL with a piece.
        for ( $i = 1; $i <= 8; $i++ ) {
            for ( $j = 1; $j <= 8; $j++ ) {
                $this->board[$i][$j] = NULL;
            }
        }

        // Create $rank variables with strings that look like this
            // rnbqkbnr
            // pppppppp
            // 8
            // PPPPPPPP
            // RNBQKBNR
            // 2p5
        // The numbers are the # of blank squares from left to right
        $rank = array();
        for ( $i = 1; $i <= 8; $i++ ) {
            // Match string = 1, but rank = 8. Fix it here to avoid headaches.
            $rank = $this->invert_rank_or_file_number($i);
            $rank_string[$rank] = $matches[$i];
        }

        // Process $rank variable strings, convert to pieces and add them to $this->board[][]
        foreach ( $rank_string as $rank => $string ) {
            $file = 1;

            for ( $i = 1; $i <= strlen($string); $i++ ) {
                $char = substr($string, $i - 1, 1);

                // Don't use is_int here. $char is a string. Use is_numeric instead.
                if ( is_numeric($char) ) {
                    $file = $file + $char;
                } else {
                    $square = $this->number_to_file($file) . $rank;

                    if ( ctype_upper($char) ) {
                        $color = 'white';
                    } else {
                        $color = 'black';
                    }

                    $type = self::PIECE_LETTERS[strtolower($char)];

                    $this->board[$rank][$file] = new ChessPiece($color, $square, $type);

                    $file++;
                }
            }
        }

        // ******* SET COLOR TO MOVE *******
        if ( $matches[9] == 'w' ) {
            $this->color_to_move = 'white';
        } elseif ( $matches[9] == 'b' ) {
            $this->color_to_move = 'black';
        } else {
            throw new Exception('ChessBoard Class - Invalid FEN - Invalid Color To Move');
        }

        // Set all castling to false. Only set to true if letter is present in FEN. Prevents bugs.
        $this->castling['white_can_castle_kingside'] = FALSE;
        $this->castling['white_can_castle_queenside'] = FALSE;
        $this->castling['black_can_castle_kingside'] = FALSE;
        $this->castling['black_can_castle_queenside'] = FALSE;

        // ******* SET CASTLING POSSIBILITIES *******
        // strpos is case sensitive, so that's good
        if ( strpos($matches[10], 'K') !== FALSE ) {
            $this->castling['white_can_castle_kingside'] = TRUE;
        }

        if ( strpos($matches[10], 'Q') !== FALSE ) {
            $this->castling['white_can_castle_queenside'] = TRUE;
        }

        if ( strpos($matches[10], 'k') !== FALSE ) {
            $this->castling['black_can_castle_kingside'] = TRUE;
        }

        if ( strpos($matches[10], 'q') !== FALSE ) {
            $this->castling['black_can_castle_queenside'] = TRUE;
        }

        // ******* SET EN PASSANT TARGET SQUARE *******
        if ( $matches[11] == '-' ) {
            $this->en_passant_target_square = FALSE;
        } else {
            $this->en_passant_target_square = new ChessSquare($matches[11]);
        }
        // ChessPiece throws its own exceptions, so no need to throw one here.

        // ******* SET HALFMOVE CLOCK *******
        $this->halfmove_clock = $matches[12];

        // ******* SET FULLMOVE NUMBER *******
        $this->fullmove_number = $matches[13];

        // ******* SET HALFMOVE NUMBER *******
        $this->halfmove_number = $matches[13] * 2 - 1;
        if ( $this->color_to_move == 'black' ) {
            $this->halfmove_number++;
        }

        $this->fen = $fen;
    }

    function get_fen() {
        $string = '';

        // A chessboard looks like this
            // a8 b8 c8 d8
            // a7 b7 c7 d7
            // etc.
        // But we want to print them starting with row 8 first.
        // So we need to adjust the loops a bit.

        for ( $rank = 8; $rank >= 1; $rank-- ) {
            $empty_squares = 0;

            for ( $file = 1; $file <= 8; $file++ ) {
                $piece = $this->board[$rank][$file];

                if ( ! $piece ) {
                    $empty_squares++;
                } else {
                    if ( $empty_squares ) {
                        $string .= $empty_squares;
                        $empty_squares = 0;
                    }
                    $string .= $piece->get_fen_symbol();
                }
            }

            if ( $empty_squares ) {
                $string .= $empty_squares;
            }

            if ( $rank != 1 ) {
                $string .= "/";
            }
        }

        if ( $this->color_to_move == 'white' ) {
            $string .= " w ";
        } elseif ( $this->color_to_move == 'black' ) {
            $string .= " b ";
        }

        if ( $this->castling['white_can_castle_kingside'] ) {
            $string .= "K";
        }

        if ( $this->castling['white_can_castle_queenside'] ) {
            $string .= "Q";
        }

        if ( $this->castling['black_can_castle_kingside'] ) {
            $string .= "k";
        }

        if ( $this->castling['black_can_castle_queenside'] ) {
            $string .= "q";
        }

        if (
            ! $this->castling['white_can_castle_kingside'] &&
            ! $this->castling['white_can_castle_queenside'] &&
            ! $this->castling['black_can_castle_kingside'] &&
            ! $this->castling['black_can_castle_queenside']
        ) {
            $string .= "-";
        }

        if ( $this->en_passant_target_square ) {
            $string .= " " . $this->en_passant_target_square->alphanumeric;
        } else {
            $string .= " -";
        }

        $string .= " " . $this->halfmove_clock . ' ' . $this->fullmove_number;

        return $string;
    }

    function update_fen() {
        $this->fen = $this->get_fen();
    }

    // Keeping this for debug reasons.
    function get_ascii_board() {
        $string = '';

        if ( $this->color_to_move == 'white' ) {
            $string .= "White To Move";
        } elseif ( $this->color_to_move == 'black' ) {
            $string .= "Black To Move";
        }

        // A chessboard looks like this
            // a8 b8 c8 d8
            // a7 b7 c7 d7
            // etc.
        // But we want to print them starting with row 8 first.
        // So we need to adjust the loops a bit.

        for ( $rank = 8; $rank >= 1; $rank-- ) {
            $string .= "<br />";

            for ( $file = 1; $file <= 8; $file++ ) {
                $square = $this->board[$rank][$file];

                if ( ! $square ) {
                    $string .= "*";
                } else {
                    $string .= $this->board[$rank][$file]->get_unicode_symbol();
                }
            }
        }
        $string .= "<br /><br />";

        return $string;
    }

    function get_graphical_board() {
        // We need to throw some variables into an array so our view can build the board.
        // The array shall be in the following format:
            // square_color = black / white
            // id = a1-h8
            // piece = HTML unicode for that piece

        // A chessboard looks like this
            // a8 b8 c8 d8
            // a7 b7 c7 d7
            // etc.
        // But we want to print them starting with row 8 first.
        // So we need to adjust the loops a bit.

        $graphical_board_array = array();
        for ( $rank = 8; $rank >= 1; $rank-- ) {
            for ( $file = 1; $file <= 8; $file++ ) {
                $piece = $this->board[$rank][$file];

                // SQUARE COLOR
                if ( ($rank + $file) % 2 == 1 ) {
                    $graphical_board_array[$rank][$file]['square_color'] = 'white';
                } else {
                    $graphical_board_array[$rank][$file]['square_color'] = 'black';
                }

                // ID
                $file_letters = new Dictionary(array(
                    1 => 'a',
                    2 => 'b',
                    3 => 'c',
                    4 => 'd',
                    5 => 'e',
                    6 => 'f',
                    7 => 'g',
                    8 => 'h'
                ));
                $graphical_board_array[$rank][$file]['id'] = $file_letters->check_dictionary($file) . $rank;

                // PIECE
                if ( ! $piece ) {
                    $graphical_board_array[$rank][$file]['piece'] = '';
                } else {
                    $graphical_board_array[$rank][$file]['piece'] = $this->board[$rank][$file]->get_unicode_symbol();
                }
            }
        }

        return $graphical_board_array;
    }

    function get_side_to_move_string() {
        $string = '';

        if ( $this->color_to_move == 'white' ) {
            $string .= "White To Move";
        } elseif ( $this->color_to_move == 'black' ) {
            $string .= "Black To Move";
        }

        return $string;
    }

    function get_who_is_winning_string() {
        $points = 0;

        foreach ( $this->board as $key1 => $value1 ) {
            foreach ( $value1 as $key2 => $piece ) {
                if ( $piece ) {
                    $points += $piece->value;
                }
            }
        }

        if ( $points > 0 ) {
            return "Material: White Ahead By $points";
        } elseif ( $points < 0 ) {
            $points *= -1;
            return "Material: Black Ahead By $points";
        } else {
            return "Material: Equal";
        }
    }

    function invert_rank_or_file_number($number) {
        $dictionary = array(
            1 => 8,
            2 => 7,
            3 => 6,
            4 => 5,
            5 => 4,
            6 => 3,
            7 => 2,
            8 => 1
        );

        return $dictionary[$number];
    }

    function number_to_file($number) {
        $dictionary = array(
            1 => 'a',
            2 => 'b',
            3 => 'c',
            4 => 'd',
            5 => 'e',
            6 => 'f',
            7 => 'g',
            8 => 'h'
        );

        if ( ! array_key_exists($number, $dictionary) ) {
            throw new Exception('ChessBoard Class - number_to_file - unknown file number - $number = ' . var_export($number, TRUE));
        }

        return $dictionary[$number];
    }

    // Note: This does not check for and reject illegal moves. It is up to code in the ChessGame class to generate a list of legal moves, then only make_move those moves.
    // In fact, sometimes make_move will be used on illegal moves (king in check moves), then the illegal moves will be deleted from the list of legal moves in a later step.
    function make_move($old_square, $new_square) {
        $moving_piece = clone $this->board[$old_square->rank][$old_square->file];

        $this->set_en_passant_target_square(NULL);

        $is_capture = $this->board[$new_square->rank][$new_square->file];

        if (
            $moving_piece->type == 'pawn' ||
            $is_capture
        ) {
            $this->halfmove_clock = 0;
        } else {
            $this->halfmove_clock++;
        }

        $this->board[$new_square->rank][$new_square->file] = $moving_piece;

        // Update $moving_piece->square too to avoid errors.
        $moving_piece->square = $new_square;

        $this->board[$old_square->rank][$old_square->file] = NULL;

        if ( $this->color_to_move == 'black' ) {
            $this->fullmove_number++;
        }

        $this->flip_color_to_move();

        $this->update_fen();
    }

    // Used to move the rook during castling.
    // Can't use make_move because it messes up color_to_move, halfmove, and fullmove.
    function make_additional_move_on_same_turn($old_square, $new_square) {
        $moving_piece = clone $this->board[$old_square->rank][$old_square->file];

        $this->board[$new_square->rank][$new_square->file] = $moving_piece;

        // Update $moving_piece->square too to avoid errors.
        $moving_piece->square = $new_square;

        $this->board[$old_square->rank][$old_square->file] = NULL;

        $this->update_fen();
    }

    function flip_color_to_move() {
        if ( $this->color_to_move == 'white' ) {
            $this->color_to_move = 'black';
        } elseif ( $this->color_to_move == 'black' ) {
            $this->color_to_move = 'white';
        }
    }

    function set_castling($string, $boolean) {
        $this->castling[$string] = $boolean;
        $this->update_fen();
    }

    function set_en_passant_target_square($square) {
        $this->en_passant_target_square = $square;
        $this->update_fen();
    }

    function square_is_occupied($square) {
        $rank = $square->rank;
        $file = $square->file;

        if ( $this->board[$rank][$file] ) {
            return TRUE;
        } else {
            return FALSE;
        }
    }

    function get_king_square($color) {
        foreach ( $this->board as $key => $value ) {
            foreach ( $value as $key2 => $piece ) {
                if ( $piece ) {
                    if ( $piece->type == 'king' && $piece->color == $color ) {
                        return $piece->square;
                    }
                }
            }
        }

        return NULL;
    }

    function remove_piece_from_square($square) {
        $rank = $square->rank;
        $file = $square->file;

        $this->board[$rank][$file] = NULL;

        $this->update_fen();
    }
}

ChessPiece.php

<?php

class ChessPiece
{
    var $value;
    var $color;
    var $type;
    var $square;

    const VALID_COLORS = array('white', 'black');
    const VALID_TYPES = array('pawn', 'knight', 'bishop', 'rook', 'queen', 'king');
    const UNICODE_CHESS_PIECES = array(
        'white_king' => '&#9812;',
        'white_queen' => '&#9813;',
        'white_rook' => '&#9814;',
        'white_bishop' => '&#9815;',
        'white_knight' => '&#9816;',
        'white_pawn' => '&#9817;',
        'black_king' => '&#9818;',
        'black_queen' => '&#9819;',
        'black_rook' => '&#9820;',
        'black_bishop' => '&#9821;',
        'black_knight' => '&#9822;',
        'black_pawn' => '&#9823;'
    );
    const FEN_CHESS_PIECES = array(
        'white_king' => 'K',
        'white_queen' => 'Q',
        'white_rook' => 'R',
        'white_bishop' => 'B',
        'white_knight' => 'N',
        'white_pawn' => 'P',
        'black_king' => 'k',
        'black_queen' => 'q',
        'black_rook' => 'r',
        'black_bishop' => 'b',
        'black_knight' => 'n',
        'black_pawn' => 'p'
    );
    const PIECE_VALUES = array(
        'pawn' => 1,
        'knight' => 3,
        'bishop' => 3,
        'rook' => 5,
        'queen' => 9,
        'king' => 0
    );
    const SIDE_VALUES = array(
        'white' => 1,
        'black' => -1
    );

    function __construct($color, $square_string, $type) {
        if ( in_array($color, self::VALID_COLORS) ) {
            $this->color = $color;
        } else {
            throw new Exception('ChessPiece Class - Invalid Color');
        }

        $this->square = new ChessSquare($square_string);

        if ( in_array($type, self::VALID_TYPES) ) {
            $this->type = $type;
        } else {
            throw new Exception('ChessPiece Class - Invalid Type');
        }

        $this->value = self::PIECE_VALUES[$type] * self::SIDE_VALUES[$color];
    }

    function __clone() {
        $this->square = clone $this->square;
    }

    function get_unicode_symbol()
    {
        $dictionary_key = $this->color . '_' . $this->type;

        return self::UNICODE_CHESS_PIECES[$dictionary_key];
    }

    function get_fen_symbol()
    {
        $dictionary_key = $this->color . '_' . $this->type;

        return self::FEN_CHESS_PIECES[$dictionary_key];
    }

    function on_rank($rank)
    {
        if ( $rank == $this->square->rank ) {
            return TRUE;
        } else {
            return FALSE;
        }
    }
}

ChessSquare.php

<?php

class ChessSquare {
    const VALID_SQUARES = array(
        'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8',
        'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8',
        'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8',
        'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8',
        'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8',
        'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8',
        'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g7', 'g8',
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8'
    );

    var $rank;
    var $file;
    var $alphanumeric;

    function __construct($alphanumeric) {
        $file_letters = new Dictionary(array(
            1 => 'a',
            2 => 'b',
            3 => 'c',
            4 => 'd',
            5 => 'e',
            6 => 'f',
            7 => 'g',
            8 => 'h'
        ));

        if ( in_array($alphanumeric, self::VALID_SQUARES) ) {
            $this->alphanumeric = $alphanumeric;

            $this->file = $file_letters->check_dictionary(substr($alphanumeric, 0, 1));
            $this->rank = substr($alphanumeric, 1, 1);
        } else {
            throw new Exception("ChessSquare Class - Invalid Square - \$alphanumeric = " . var_export($alphanumeric, TRUE));
        }
    }
}

Dictionary.php

<?php

// Array of pairs. The list of both columns put together cannot contain duplicates.
class Dictionary {
    var $array_of_pairs;

    function __construct($array) {
        // make sure there are no duplicates

        $this->array_of_pairs = $array;
    }

    function check_dictionary($search_key) {
        if ( isset($this->array_of_pairs[$search_key]) ) {
            return $this->array_of_pairs[$search_key];
        } elseif ( $search_results = array_search($search_key, $this->array_of_pairs) ) {
            return $search_results;
        } else {
            return NULL;
        }
    }
}

script.js

$(document).ready(function(){
    $('select').dblclick(function(){
        $('#make_move').submit();
    });

    $('.draggable_piece').on("dragstart", function (event) {
        var dt = event.originalEvent.dataTransfer;
        dt.setData('Text', $(this).closest('td').attr('id'));
    });

    $('table td').on("dragenter dragover drop", function (event) {  
        event.preventDefault();

        if (event.type === 'drop') {
            var oldsquare = event.originalEvent.dataTransfer.getData('Text',$(this).attr('id'));

            var newsquare = $(this).attr('id');

            var coordinate_notation = oldsquare + newsquare;

            var option_tag_in_select_tag = $("select[name='move'] option[data-coordinate-notation='" + coordinate_notation + "']");

            if ( option_tag_in_select_tag.length != 0 ) {
                option_tag_in_select_tag.attr('selected','selected');

                $('#make_move').submit();
            }
        };
    });
})

style.css

body {
    font-family:sans-serif;
}

input[name="fen"] {
    width: 500px;
}

input[type="submit"],
input[type="button"] {
    font-size: 12pt;
}

textarea[name="pgn"] {
    width: 500px;
    font-family: sans-serif;
}

select[name="move"] {
    width: 8em;
}

.two_columns {
    display: flex;
    width: 600px;
}

.two_columns>div:nth-child(1) {
    flex: 60%;
}

.two_columns>div:nth-child(2) {
    flex: 40%;
}

#graphical_board {
    table-layout: fixed;
    border-collapse: collapse;
}

#graphical_board td {
    height: 40px;
    width: 40px;
    padding: 0;
    margin: 0;
    text-align: center;
    vertical-align: middle;
    font-size: 30px;
    font-weight: bold;
    font-family: "Arial Unicode MS", "Lucida Console", Courier, monospace;
    cursor: move;
}

.black {
    background-color: #769656;
}

.white {
    background-color: #EEEED2;
}

.status_box {
    background-color: #F0F0F0;
    border: 1px solid black;
    padding-top: 2px;
    padding-bottom: 2px;
    padding-left: 4px;
    padding-right: 4px;
    width: 310px;
    margin-bottom: 5px;
}

view.php

<!DOCTYPE html>

<html lang="en-us">
    <head>
        <meta charset="utf-8" />

        <title>
            AdmiralAdama Chess
        </title>

        <link rel="stylesheet" href="style.css">

        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

        <script src="scripts.js"></script>
    </head>

    <body>
        <h1>
        AdmiralAdama Chess
        </h1>

        <div class="two_columns">
            <div>
                <div class="status_box">
                    <?php echo $side_to_move; ?>

                </div>

                <div class="status_box">
                    <?php echo $who_is_winning; ?>

                </div>

                <table id="graphical_board">
                    <tbody>
                        <?php foreach ( $graphical_board_array as $rank => $row ): ?>

                            <tr>
                                <?php foreach ( $row as $file => $column ): ?>

                                    <td
                                        id ="<?php echo $column['id']; ?>"
                                        class="<?php echo $column['square_color']; ?>"
                                    >
                                        <span
                                            class="draggable_piece"
                                            draggable="true"
                                        >
                                            <?php echo $column['piece']; ?>

                                        </span>
                                    </td>
                                <?php endforeach; ?>

                            </tr>
                        <?php endforeach; ?>

                    </tbody>
                </table>
                <!-- <input type="submit" name="flip" value="Flip The Board" /> -->
                <input type="button" onclick="window.location='.'" value="Reset The Board" />
            </div>

            <div>
                <form id="make_move">
                    Legal Moves:<br />
                    <select name="move" size="19">
                        <?php foreach ( $legal_moves as $key => $move ): ?>

                            <option
                                value="<?php echo $move->board->fen; ?>"
                                data-coordinate-notation="<?php echo $move->coordinate_notation; ?>"
                            >
                                <?php echo $move->notation; ?>

                            </option>
                        <?php endforeach; ?>

                    </select><br />
                    Move Count: <?php echo count($legal_moves); ?><br />

                    <?php

                    $time = microtime();
                    $time = explode(' ', $time);
                    $time = $time[1] + $time[0];
                    $finish = $time;
                    $total_time = round(($finish - $start), 4);

                    $total_time *= 1000;
                    $total_time = round($total_time);

                    ?>

                    Load Time: <?php echo $total_time; ?> ms<br />
                    <input type="submit" value="Make Move" />
                </form>
            </div>
        </div>

        <form id="import_fen">
            <p>
                FEN:<br />
                <input type="text" name="fen" value="<?php echo $fen; ?>" /><br />
                <input type="submit" value="Import FEN" />
            </p>
        </form>
    </body>
</html>
\$\endgroup\$

2 Answers 2

5
+50
\$\begingroup\$

General Feedback

Large Methods

Some of the class methods are really large - e.g. ChessGame ::get_legal_moves_list() which consumes ~150 lines. There is a lot of redundancy - especially in the code to assign $directions_list. That code should be moved out to a separate method. Generally whenever a method goes beyone ~10-15 lines you should consider refactoring it. In addition to readability it will also help with testing.

In that method, there are 4 calls to ChessGame::add_slide_and_slidecapture_moves_to_moves_list() and those could all likely be simplified to a single call whenever the piece type is bishop, rook, queen or king.

//the array below could be stored in a constant - e.g. CAPTURABLE_PIECE_TYPES
else if ( in_array($piece->type, array('bishop', 'rook', 'queen', 'king')) {
    //getDirectionsListForPiece is a method that could accept the piece object, or it may be simpler to pass $piece->type
    $directions_list = $this->getDirectionsListForPiece($piece); 
    $moves = $this->add_slide_and_slidecapture_moves_to_moves_list($directions_list, 7, $moves, $piece, $color_to_move, $board);
}

Because $directions_list doesn't appear to be used later in that method, perhaps it could be removed from the list of arguments for all methods that accept it, and those methods could fetch it when necessary.

Redundancies

Looking at those methods like ChessGame::get_squares_in_these_directions(), ChessGame::add_capture_moves_to_moves_list(), ChessGame::add_slide_and_slidecapture_moves_to_moves_list() we see the array $xy declared - sometimes even in a foreach loop!:

$xy = array(
    'north' => array(0,1),
    'south' => array(0,-1),
    'east' => array(1,0),
    'west' => array(-1,0),
    'northeast' => array(1,1),
    'northwest' => array(-1,1),
    'southeast' => array(1,-1),
    'southwest' => array(-1,-1)
);

This goes against the Don't Repeat Yourself principle (i.e. D.R.Y). It would be wise to put that initial value in a constant - maybe even store the value after it is flipped (via ChessGame::convert_from_xy_to_rankfile()) in a static property...

Also in ChessGame::try_to_make_square_using_rank_and_file_num() there is an assignment of $valid_squares that is never modified and appears to be identical to ChessSquare::VALID_SQUARES. It would be wise to re-use that constant in ChessGame::try_to_make_square_using_rank_and_file_num()

I see other redundancies, like whenever a Dictionary() object is created, it is typically passed an array like below:

$file_letters = new Dictionary(array(
    1 => 'a',
    2 => 'b',
    3 => 'c',
    4 => 'd',
    5 => 'e',
    6 => 'f',
    7 => 'g',
    8 => 'h'
));

I see three occurrences where such an object is assigned to $file_letters- one in ChessGame::try_to_make_square_using_rank_and_file_num(), one in ChessBoard::get_graphical_board() and ChessSquare::__construct(), plus that array is used in ChessBoard::number_to_file(). It would be wise to store that array in a constant somewhere, and perhaps make one dictionary to be used in multiple places. There are multiple ways to handle that - e.g. a static property could be made on the game class and referenced wherever necessary, or a static method could be made on the game class that would utilize that static property. Though maybe you should evaluate if it is worth having a separate Dictionary class with a single method - maybe it would be simpler just to have a (static) method somewhere that would handle that functionality...

For more tips on cleaning up code, check out this video of a presentation Rafael Dohms talk about cleaning up code (or see the slides here).

Exception Handling

Exceptions are thrown but never caught. Currently if an exception is thrown, that is displayed to the user, like the one I see when I change the URL of a move:

Fatal error: Uncaught exception 'Exception' with message 'ChessBoard Class - Invalid FEN' in /home/clania/clania.net/admiraladama/chess_v2/ChessBoard.php:54 Stack trace: #0 /home/clania/clania.net/admiraladama/chess_v2/index.php(23): ChessBoard->set_fen('rnbqkbnr/pppp1p...') #1 {main} thrown in /home/clania/clania.net/admiraladama/chess_v2/ChessBoard.php on line 54

Ideally those would be handled - perhaps in Index.php with a try/catch that may set the value of an error variable and the view would display that error message instead of the board.

PHP mixed within HTML

Generally it is best to separate the business logic (PHP) from the view (HTML). Using a template engine could help for this aspect - e.g. Smarty, Twig, etc. For more information on this topic, prefer to this article.

Form submission method

Did you consider using post for the form method? If the form was submitted as POST requests, then the form variables would not be in the query string, and the user might be less apt to modify them. The effect would be that on the server side $_POST would need to be used instead of $_GET.

More Specific points

PHP

Declaring Instance/Member variables

The class definitions appear to use the PHP 4 style declarations for instance variables (i.e. var). Note that while support of this syntax isn't currently deprecated, it may be in the future:

Note: The PHP 4 method of declaring a variable with the var keyword is still supported for compatibility reasons (as a synonym for the public keyword). In PHP 5 before 5.1.3, its usage would generate an E_STRICT warning.1

Getting the timestamp with microseconds

Index.php starts with the following lines:

$time = microtime();
$time = explode(' ', $time);
$time = $time[1] + $time[0];
$start = $time;

Some would argue that readability suffers here because on the first line, $time is a string, then on the second line it is an array and then on the third line it is a string again.

This appears to function identically to the Example 1: Example #1 Timing script execution with microtime() on the PHP Documentation for microtime(). Notice that the function in that example contains the comment:

/**
 * Simple function to replicate PHP 5 behaviour
 */

And notice the next example is titled Example #2 Timing script execution in PHP 5, where it simply calls microtime() with true for the parameter get_as_float. So instead of using those first three lines of Index.php could simply be replaced with the call to microtime(true) for the same functionality.

$start = microtime(true);

The same applies to the identical code within View.php.

Error Reporting

Index.php also contains the following line:

error_reporting(-1);

According to the code in Example #1 error_reporting() examples in the documentation for error_reporting(), E_ALL can also be used for that (presuming the PHP version is 5.4 or greater - otherwise E_STRICT won't be included in that so you could use error_reporting(E_ALL | E_STRICT);):

// Report all PHP errors (see changelog)
error_reporting(E_ALL);

// Report all PHP errors
error_reporting(-1);    

Using the constant like that (i.e. error_reporting(E_ALL);) is more readable for anyone who doesn't remember what -1 signifies when passed to that

JS

I like how the drag and drop code is effective and concise - typically such code ends up being overly-complex. And I often recommend DOM references (e.g. $('#make_move') be stored in a variable (or const if is used) but there are so few of them and most of the moves require a page-load that it likely wouldn't be worth it.

DOM ready function

the documentation for .ready() states:

jQuery offers several ways to attach a function that will run when the DOM is ready. All of the following syntaxes are equivalent:

  • $( handler )
  • $( document ).ready( handler )
  • $( "document" ).ready( handler )
  • $( "img" ).ready( handler )
  • $().ready( handler )

As of jQuery 3.0, only the first syntax is recommended; the other syntaxes still work but are deprecated.2

So the first line of the JavaScript file:

$(document).ready(function(){

Can be simplified to:

$(function(){

CSS

Simplification of padding for status box

The four padding styles can be simplified from:

padding-top: 2px;
padding-bottom: 2px;
padding-left: 4px;
padding-right: 4px;

To the following:

padding: 2px 4px;

Because:

When two values are specified, the first padding applies to the top and bottom, the second to the left and right. 3

1http://php.net/manual/en/language.oop5.visibility.php#language.oop5.basic.class.class.name
2http://api.jquery.com/ready/
3https://developer.mozilla.org/en-US/docs/Web/CSS/padding#Syntax

\$\endgroup\$
7
  • 1
    \$\begingroup\$ Thanks for your feedback. I found the presentation you linked helpful. I guess I am looking for refactoring tips more than anything. Can you elaborate on this comment? There are 4 calls to ::add_slide_and_slidecapture_moves_to_moves_list() and those could all likely be simplified to a single call whenever the piece type is bishop, rook, queen or king. \$\endgroup\$ Commented Sep 11, 2018 at 20:08
  • 1
    \$\begingroup\$ Noted- I updated that section \$\endgroup\$ Commented Sep 11, 2018 at 21:32
  • 1
    \$\begingroup\$ @AdmiralAdama I have started to expand that section about the large methods and added another section about redundancies - this is just a start and definitely not exhaustive. I also added a section for the Javascript \$\endgroup\$ Commented Sep 12, 2018 at 16:12
  • \$\begingroup\$ Any tips for performance? Should I get an IDE with a profiler and figure out which methods are being slow? A good chess engine can analyze about 1 million nodes per second. My code is doing like 0.66 nodes per second. \$\endgroup\$ Commented Sep 13, 2018 at 19:39
  • \$\begingroup\$ Yes that would be a good place to start. Honestly I feel that this code is so complex that I don't fully understand it yet. Perhaps a goal of reducing the number of objects created and passed around would be ideal. That might mean taking a completely different approach than the current implementation, using arrays instead of objects, etc. \$\endgroup\$ Commented Sep 13, 2018 at 19:45
4
\$\begingroup\$

Performance

As mentioned in the original question, my code is SLOW. It takes 1500ms to calculate approximately 30 moves.

This is way too slow for this code to be used in a chess engine. Chess engines (A.I.) need to be able to calculate hundreds of thousands of moves or more per second.

I installed WAMPP (Apache + PHP) on my computer, installed XDebug, loaded my chess game with the profiler on, then viewed the profiler log in QCacheGrind.

Biggest performance gain (-942ms)

I was expecting something like the ChessSquare class to be the slowest, because it is created so many times. But the results were very surprising. 45% of the calculation time is being spent on calculating the FEN for every chessboard created. (The FEN is a string that summarizes that particular chessboard's position in text format.)

Screenshot of Xdebug profiler

This is NOT what I expected to be taking the majority of the calculation time. And it's easy to refactor the code to only calculate the FEN about 30 times (instead of the 6,775 times in the screenshot).

I deleted the ChessBoard->fen variable, then I deleted most of the calls to update_fen and get_fen. By not having to keep the ChessBoard->fen variable up to date every single time the ChessBoard's internal variables change, we save a lot of calculations.

  • Original Code: 1529 ms
  • New Code: 587ms

As you can see, this modification has made the code 2.6x faster.

Second biggest performance gain (-306 ms)

I kept tinkering with the code and realized that I do not need to place the entire ChessBoard in each move, if the move we are generating is a second layer move (just used to get threatened squares). I refactored the constructor of the ChessMove class and wrapped some of the $board stuff in a conditional.

function __construct(
    $starting_square,
    $ending_square,
    $color,
    $piece_type,
    $capture,
    $old_board,
    $store_board = TRUE
) {
    $this->starting_square = $starting_square;
    $this->ending_square = $ending_square;
    $this->color = $color;
    $this->piece_type = $piece_type;
    $this->capture = $capture;

    // These cases are rare. The data is passed in via set functions instead of in the constructor.
    $this->disambiguation = '';
    $this->promotion_piece_type = NULL;
    $this->en_passant = FALSE;
    $this->check = FALSE;
    $this->checkmate = FALSE;

    if ( $store_board ) {
        $this->board = clone $old_board;
        $this->board->make_move($starting_square, $ending_square);

        $this->possibly_remove_castling_privileges();

        $this->if_castling_move_rook();
    }
}

This shaved off 306 ms. Awesome!

Current load time: 281 ms

Other optimizations (-150 ms)

  • Use $haystack[needle] instead of array_search($needle, $haystack).
  • Simplify square_exists_and_not_occupied_by_friendly_piece. I don't need to compare it to a giant array of legal squares. I just need to check that $rank and $file are >= 1 and <= 8.
  • Move certain arrays to constants (mentioned in Sam Onela's answer), especially since they are currently inside of loops.
  • Delete ChessMove->notation and ChessMove->coordinate_notation, and use a getter instead. That way we don't need to update them every time ChessMove's internal variables change.
  • Delete Dictionary and use constants and array functions instead.
  • Replaced if ( is_a($square, 'ChessPiece') ) with if ( $square )

Current load time: 131 ms

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.