1
\$\begingroup\$

I have been really into chess lately so I decided to program the game in PHP for practice. I want to get more comfortable with classes.

Part 1 of the project is complete. I have a working ASCII board that displays the pieces, and I have an import tool that can import a board. (In chess, they use FEN Notation for importing boards)

I decided to create 2 classes: ChessGame and ChessPiece. I experimented with making classes like Bishop, Knight, etc. that extended ChessPiece, but it ended up being messy so I deleted them.

I'm looking for feedback on how I structured the classes, and feedback on how I should structure classes that I add later. For example, I am going to make the game playable, so I need to program in all the piece movement rules somewhere. Should that get its own class? Should I program that all into ChessGame? Can I program the movement rules into a class for each piece instead, without making my code hard to read and disorganized?

Also, feel free to suggest any other tips you might have to make my code better organized and more readable.

If this Code Review is well received, I will post additional code later on for gameplay/piece movement, list of legal moves, GUI improvements, and eventually porting the game to C# and JavaScript so I can practice coding in those languages.

Website

http://admiraladama.dreamhosters.com/PHPChess

Screenshot

AdmiralAdama chess screenshot

index.php

<?php

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

require_once('classes.php');

$game = new ChessGame();

if ( isset($_POST['fen']) && isset($_POST['import_fen']) )
{
    $game->set_fen($_POST['fen']);
}

$fen = $game->get_fen();

$ascii_board = $game->get_ascii_board();

?><!DOCTYPE html>

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

        <title>
            AdmiralAdama Chess
        </title>

        <style>
            input[name="fen"] {
                width:56em;
            }

            textarea[name="ascii_board"] {
                font-family:"Arial Unicode MS", "Lucida Console", Courier, monospace;
                font-size:26pt;
                width:325px;
                height:435px;
            }
        </style>
    </head>

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

        <form method="post">
            <p>
                FEN:<br />
                <input type="text" name="fen" value="<?php echo $fen; ?>" />
            </p>

            <p>
            <input type="submit" name="import_fen" value="Import FEN" />
            </p>

            <p>
                ASCII board:<br />
                <textarea name="ascii_board" rows="9"><?php echo $ascii_board; ?></textarea>
            </p>
        </form>
    </body>
</html>

classes.php

<?php

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

    return $dictionary[$number];
}

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];
}

class ChessGame {
    const DEFAULT_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
    const VALID_COLORS = array('white', 'black');
    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'
    );
    const UNICODE_EMPTY_SQUARE = '&#9633;';

    // $board[y][x], or in this case, $board[rank][file]
    var $board = array();
    var $color_to_move;
    var $white_can_castle_kingside;
    var $white_can_castle_queenside;
    var $black_can_castle_kingside;
    var $black_can_castle_queenside;
    var $en_passant_target_square = NULL;
    var $halfmove_clock;

    // Not sure why this is in a FEN. I googled it and the answer is...
    // "Full move counter is common in chess books where the games start from a diagram."
    var $fullmove_number;

    function __construct() {
        $this->set_fen(self::DEFAULT_FEN);
    }

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

        // 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 = 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 = number_to_file($file) . $rank;

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

                    $dictionary = array(
                        'p' => 'pawn',
                        'n' => 'knight',
                        'b' => 'bishop',
                        'r' => 'rook',
                        'q' => 'queen',
                        'k' => 'king'
                    );

                    $type = $dictionary[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->white_can_castle_kingside = FALSE;
        $this->white_can_castle_queenside = FALSE;
        $this->black_can_castle_kingside = FALSE;
        $this->black_can_castle_queenside = FALSE;

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

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

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

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

        // ******* SET EN PASSANT TARGET SQUARE *******
        if ( $matches[11] == '-' ) {
            $this->en_passant_target_square = FALSE;
        } elseif ( in_array($matches[11], self::VALID_SQUARES) ) {
            $this->en_passant_target_square = $matches[11];
        } else {
            throw new Exception('ChessPiece Class - Invalid FEN - Invalid En Passant Target Square');
        }

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

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

    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++ ) {
                $square = $this->board[$rank][$file];

                if ( ! $square ) {
                    $empty_squares++;
                } else {
                    if ( $empty_squares ) {
                        $string .= $empty_squares;
                        $empty_squares = 0;
                    }
                    $string .= $this->board[$rank][$file]->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->white_can_castle_kingside ) {
            $string .= "K";
        }

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

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

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

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

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

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

        return $string;
    }

    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 .= "\r\n";

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

                if ( ! $square ) {
                    $string .= self::UNICODE_EMPTY_SQUARE;
                } else {
                    $string .= $this->board[$rank][$file]->get_unicode_symbol();
                }
            }
        }

        return $string;
    }
}

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

    const VALID_COLORS = array('white', 'black');
    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'
    );
    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'
    );

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

        if ( in_array($square, self::VALID_SQUARES) ) {
            $this->square = $square;
        } else {
            throw new Exception('ChessPiece Class - Invalid Square');
        }

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

    function print_basic_info() {
        $basic_info = '';

        $basic_info .= 'Point Value: ' . $this->point_value . "<br />";
        $basic_info .= 'Color: ' . $this->color . "<br />";
        $basic_info .= 'Type: ' . $this->type . "<br />";
        $basic_info .= 'Square: ' . $this->square . "<br />";

        echo($basic_info);
    }

    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];
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.