Sample Application: A Chess Board Simulator

This example implements a very crude (but workable) means for two people to play chess on the Web, using the GD module to do the actual graphical machinations involved in drawing the chess board.

The interface to the chess program is a web page that has the image of a board at the top and three forms at the bottom. The first form is for submitting a move, the second is for starting a new game, and the third is for reloading the board between moves to see if an opponent has moved. The moves are entered through the use of <SELECT> input fields, which makes for a slightly clunky interface but simplifies the example for the purposes of this chapter, as we do not have to check for valid input from the user. The interface web page is shown in Figure 4.1.

The HTML page resulting from a “new game” command

Figure 4-1. The HTML page resulting from a “new game” command

Note that this is just an example of using GD to manipulate graphics and really shouldn’t be used as a chess-playing script. The chess server uses GD’s image manipulation methods to create a new board from images of the individual chess pieces, and to move the pieces around on the board. It is the same as if you were sitting across the board from your opponent in a real-life game of chess; nothing is physically preventing you from making an illegal move. Our chess server does not implement an intelligent chess engine. The purpose here is not to demonstrate the inner workings of Deep Blue, but to show how to use GD in a practical application. However, it is still a working chess program; it’s just probably more accurate to call it a chess board “simulator.”

The submitmove.cgi script creates the HTML page that is the user interface. In turn, it calls the drawboard.cgi script that actually creates the image data and sends it to the web browser. This script uses an external file called currentgame.txt that contains information about who (white or black) is currently moving, the move number, and a list containing the history of moves. Note that in an actual production script, you should explicitly unlink this file before writing to it. Here is the code for submitmove.cgi:

#!/usr/bin/perl -w 
#
# submitmove.cgi
# Create the web page that acts as an interface to the chess board simulator.
#
use GD;
use CGI;
use Fcntl qw(:DEFAULT :flock);         # for file locking constants
use strict;

# Initialize variables
#
my ($tile, $image, @fillcolors, $startfill, $endfill);
my ($tilew, $tileh) = (40, 40);
my ($width, $height) = ($tilew*9, $tileh*9);
my ($movenum, $tomove, @history);

# Now get the parameters passed into the script.
# Use the CGI module to parse the CGI form submmission.
# If this script was called as a result of a newboard or refresh action,
# these should all be empty.
#
my $query = new CGI;
my $action = $query->param('action'); 
my $startrow = $query->param('startrow');
my $startcol = $query->param('startcol');
my $endrow = $query->param('endrow');
my $endcol = $query->param('endcol');
my $special = $query->param('special');

At this point in the execution of the script we need to read in the state of the current game in progress. There are three possible actions, depending on how the script was called:

  • Creating a new board. In this case, just reset the state of the game.

  • Making a move. If someone has made a move, the script should increment the move and toggle the player.

  • Refreshing the screen. Because the page has a very short expiration period, it will not be in the user’s cache when the user wants to reload the page. In this case, the script should not modify the current state. Note that because of the way this script is written, the browser’s Reload button should not be used to check on an opponent’s move. Instead, a link has been incorporated into the web page that will reload the page and keep its current state. Implementing the script so that the Reload button works correctly can be done, but it is even more cumbersome and beyond the scope of this example.

Continuing with the code:

if ($action eq "newboard") { 
    # Starting a new game, so reset the move number to 1 
    # and white should move first
    #
    ($movenum, $tomove) = (1, 'White'); 

} else {  
    # If we get here we are either refreshing the screen, or making a move.
    # In either case we will need to parse the game file to determine the
    # current state.
    # The current state of the game is stored in a game file called 
    # currentgame.txt. Open the game file and retrieve the current game
    # state.
    #
    open GAME, 'currentgame.txt';
    flock GAME, LOCK_EX;             # place an exclusive lock on the file
    while (<GAME>) {
        if (/^move number=(.+)$/) {
            $movenum = $1;           # the number of the current move 
        } elsif (/^to move=(.+)$/) {
            $tomove = $1;            # white or black
        } else {
            push @history, $_;       # a history of moves
        }
    }
    close GAME;       # closes and unlocks the file
    if ($action eq "move") {
        # If this script was called because a move was made,
        # we should increment the move number
        #
        ++$movenum;
        
        # We should also toggle the current player
        #
        if ($tomove eq "White") { 
            $tomove = "Black";
        } else {
            $tomove = "White";
        }
        		
        # And add the current move to the history queue
        #
        push @history, "$startrow$startcol to $endrow$endcol\n";
    }     
}

# Let's write our current state back to the game file,
# or create a game file if we're starting a new game...
#
open GAME, '>currentgame.txt';
flock GAME, LOCK_EX;              # place an exclusive lock on the file
print GAME "move number=$movenum\n";
print GAME "to move=$tomove\n";
print GAME join '', @history;
close GAME;

The remainder of the script prints the HTML for the web page. The image of the board is an <IMG> tag that calls the drawboard.cgi script with a number of parameters. The parameters will all be empty unless a move has been made. The drawboard.cgi script understands this and will interpret the parameters accordingly.

One other item of note is the “special move” input field on the first form. In chess most moves involve only one piece (except in the case of a capture, which our program deals with by simply writing one piece over another). However, there are a few moves that require that two pieces be moved within a single turn. These cases are king- and queen-side castling and the promotion of pawns to queens. Because there are only three cases, we will implement them as special cases within the drawboard.cgi script. The castling moves make the assumption that the move indicated by the player is the movement of the king, and the pawn promotion assumes that the pawn is actually in a position to be promoted to a queen. The values of “O-O” for king-side castling and “O-O-O” for queen-side castling is borrowed from Portable Game Notation (PGN), a popular form for annotating chess games.

# Let the CGI module print the header info.
# The expire parameter tells the browser to remove this script
# from its cache after 1 second to ensure that we are generating new 
# output with each invocation. We could also use a no-cache pagma.
#
print $query->header(-type    => 'text/html',
                     -expires =>'-1s',
                    );

print $query->start_html(-title => "Chess Game Move $movenum");

print "<H1>$tomove to move</H1>";

# Call the drawboard.cgi script to generate the image stream for
# the current chessboard. Pass in the current action (new, move, or
# refresh) and let the script determine which action to take.
# Note the use of the width and height parameters to the img tag...
# this allows the browser to draw the page before it has all the 
# image data in hand.
#
print "<img width=$width height=$height "; 
print "src=\"drawboard.cgi?action=$action&";
print "tilew=$tilew&tileh=$tileh&";

# The following fields will be blank if the action 
# is 'new' or 'refresh'...
#
print "startrow=$startrow&startcol=$startcol&";
print "endrow=$endrow&endcol=$endcol&special=$special\" ALIGN=left>";

# Now print the forms at the bottom of the page
#
print <<EOF;
<FORM METHOD=POST ACTION="submitmove.cgi">
<INPUT TYPE=hidden NAME=action VALUE=move>
Move the piece at<BR>
<SELECT NAME="startrow">
<OPTION>a</OPTION><OPTION>b</OPTION><OPTION>c</OPTION><OPTION>d</OPTION>
<OPTION>e</OPTION><OPTION>f</OPTION><OPTION>g</OPTION><OPTION>h</OPTION>
</SELECT>
<SELECT NAME="startcol">
<OPTION>1</OPTION><OPTION>2</OPTION><OPTION>3</OPTION><OPTION>4</OPTION>
<OPTION>5</OPTION><OPTION>6</OPTION><OPTION>7</OPTION><OPTION>8</OPTION>
</SELECT><BR>
to<BR>
<SELECT NAME="endrow">
<OPTION>a</OPTION><OPTION>b</OPTION><OPTION>c</OPTION><OPTION>d</OPTION>
<OPTION>e</OPTION><OPTION>f</OPTION><OPTION>g</OPTION><OPTION>h</OPTION>
</SELECT>
<SELECT NAME="endcol">
<OPTION>1</OPTION><OPTION>2</OPTION><OPTION>3</OPTION><OPTION>4</OPTION>
<OPTION>5</OPTION><OPTION>6</OPTION><OPTION>7</OPTION><OPTION>8</OPTION>
</SELECT><br>
Is this a special move?<BR>
<SELECT NAME="special">
<OPTION>No special move</OPTION>
<OPTION VALUE="O-O">King side castle</OPTION>
<OPTION VALUE="O-O-O">Queen side castle</OPTION>
<OPTION VALUE="PQ">Promote Pawn to Queen</OPTION>
</SELECT><BR>

<INPUT TYPE="submit" VALUE="Submit Move"><br>
</FORM>

<FORM METHOD=POST ACTION="submitmove.cgi">
<INPUT TYPE=hidden NAME=action VALUE=newboard>
Or start a new game... <BR>
<INPUT TYPE="submit" VALUE="New Game"><br>
</FORM>

<FORM METHOD=POST ACTION="submitmove.cgi">
<INPUT TYPE=hidden NAME=action VALUE=refresh>
Or reload the board...(don't use <BR>
your browser's reload button!)<BR>
<INPUT TYPE="submit" VALUE="Reload"><br>
</FORM>
</BODY>
</HTML>
EOF
# End of submitmove.cgi

The drawboard.cgi script is called whenever the board needs to be drawn. It is passed a number of parameters by submitmove.cgi; the action indicates how the board is to be drawn (draw a new board, move a piece, or simply refresh the current board), tilew and tileh give the dimensions in pixels of each square on the board (to make it easy to change the size of the board), and the starting and ending row and columns are the values entered on the corresponding fields on the web page. If the action is refresh or newboard, these fields will be empty.

This script stores the current board in an external GIF file called currentboard.gif. Here is the code for drawboard.cgi:

#!/usr/bin/perl -w 
#
# drawboard.cgi
# Dynamically create the image of a chess board.
#
use GD;
use CGI;
use Fcntl qw(:DEFAULT :flock);         # for file locking constants
use strict;

# Set up some variables
#
my ($image, @fillcolors, $startfill, $endfill);

# Set up a translation hash for changing row letters to numbers
#
my $i = 1;
my %rownums = map {$_ => $i++} ('a'..'h');

# Get the move passed from submitmove.cgi 
#
my $query = new CGI;
my $action = $query->param('action');
my $tilew = $query->param('tilew');
my $tileh = $query->param('tileh');
my $startrow =$rownums { $query->param('startrow') };
my $startcol = $query->param('startcol');
my $endrow = $rownums { $query->param('endrow') };
my $endcol = $query->param('endcol');
my $special = $query->param('special');

If the current action is newboard, create a new chess board. This script also uses twelve small GIF files, each containing the image of a single unique chess piece. These files should have two-letter names, the first letter being the color of the piece and the second being the first letter of the piece (i.e., br.gif for the black rook, wk.gif for the white king, wn.gif for the white knight, etc.). These images are shown in Figure 4.2. The pieces are black and white on an off-white background; the black squares of the chess board are actually a purplish color, and the white squares are an off-white.

The 12 chess piece tiles used to create a new chessboard, stored as separate GIF files. The files are (left to right) br.gif, bn.gif, bb.gif, bq.gif, bk.gif, bp.gif, wr.gif, wn.gif, wb.gif, wq.gif, wk.gif, and wp.gif.

Figure 4-2. The 12 chess piece tiles used to create a new chessboard, stored as separate GIF files. The files are (left to right) br.gif, bn.gif, bb.gif, bq.gif, bk.gif, bp.gif, wr.gif, wn.gif, wb.gif, wq.gif, wk.gif, and wp.gif.

if ($action eq "newboard") {
    $image = new GD::Image($tilew*9, $tileh*9);
    
    # The background is set to the first allocated color.
    # In this case, 80% gray
    # 
    $image->colorAllocate(204, 204, 204); 
    my $black = $image->colorAllocate(0, 0, 0);
    
    # Now allocate the colors for our squares
    #
    @fillcolors = (
                    $image->colorAllocate(255, 255, 204),   # (0) Off-white
                    $image->colorAllocate(204, 102, 204),   # (1) Purple 
                  );
          
    # Label the rows with letters and the columns with numbers
    #
    foreach (1..8) {
       $image->string(gdLargeFont, 
                  int($tilew/2), 
              int(($_+.5)*$tileh), 
              chr(96+$_), $black);
       $image->string(gdLargeFont, 
                  int(($_+.5)*$tilew), 
              int($tileh/2), 
              $_, $black);
    }

    # Draw a large rectangle for the light squares
    #
    $image->filledRectangle($tilew,$tileh,  
                            $tilew*9, $tileh*9,
                            $fillcolors[0] );
    
    # Draw the middle four rows of dark squares of the board
    #
    foreach my $y (3, 5) {
        foreach my $x (1, 3, 5, 7) {
            $image->filledRectangle($x*$tilew,$y*$tileh,  
                                    ($x+1)*$tilew-1, ($y+1)*$tileh-1,
                                    $fillcolors[1] );
            $image->filledRectangle(($x+1)*$tilew,($y+1)*$tileh,  
                                    ($x+2)*$tilew-1, ($y+2)*$tileh-1,
                                    $fillcolors[1] );
        }
    } 
    
    # Finally, add the pieces to the board. The pieces are stored
    # in external gif files labelled 'wp' for white pawn, 'bn' for
    # black knight, etc. Each piece will be drawn with its
    # corresponding square color.
    #
    place ('wr', 1, (1, 8));
    place ('wn', 1, (2, 7));
    place ('wb', 1, (3, 6));
    place ('wq', 1, 4);
    place ('wk', 1, 5);
    place ('wp', 2, (1, 2, 3, 4, 5, 6, 7, 8));
    place ('br', 8, (1, 8));
    place ('bn', 8, (2, 7));
    place ('bb', 8, (3, 6));
    place ('bq', 8, 4);
    place ('bk', 8, 5);
    place ('bp', 7, (1, 2, 3, 4, 5, 6, 7, 8));

In the second case, a move may have been made. First check the special cases, then move the piece from one square to another with the move( ) subroutine.

} else {
    open BOARD, "currentboard.gif";
    $image = newFromGif GD::Image(\*BOARD);
    close BOARD;
    @fillcolors = (
                    $image->colorExact(255, 255, 204),   # (0) Off-white
                    $image->colorExact(204, 102, 204),   # (1) Purple 
                  );
        
    if ($action eq "move") {
        # First parse $special to see if we have a special move
        #
    if ($special =~ /O-O-O/) {
        # 0-0-0 = Queen side castle 
        # We're assuming that the actual move is the movement
        # of the king...we just have to move the castle.
        #
        move($startrow, 1, $startrow, 4);
    }
    elsif ($special =~ /O-O/) {
        # O-O = King side castle
        #
        move($startrow, 8, $startrow, 6);
    }
    elsif ($special =~ /PQ/) {
        # Promote Pawn to Queen
        # We must trust that the current piece is a pawn,
        # replacing it at the current spot with a queen
        # before it is moved later. We are also assuming
        # that the move is valid; we use the end row value to
        # determine whether the piece should be black or white.
        #
        place('bq', $startrow, $startcol) if ($endrow ==  1);
        place('wq', $startrow, $startcol) if ($endrow == 8);
    }
        
    # Now move the piece in question
    #
    move($startrow, $startcol, $endrow, $endcol);
   }
}

Now print out the graphic:

# Write out the current board to currentboard.gif
my $gifdata = $image->gif;
open OUTFILE,">currentboard.gif"; 
flock OUTFILE, LOCK_EX;              # place an exclusive lock on the file
binmode OUTFILE;
print OUTFILE $gifdata;
close OUTFILE;

# Let CGI module print the header info
#
print $query->header(-type => 'image/gif',
                     -expires => '-1s'
                     );
binmode (STDOUT);
print $gifdata;

Three subroutines are used by the drawboard.cgi script. The first is the place( ) subroutine called to place each piece in the board when a new board is being created. The tilecolor( ) subroutine is called to determine the color of the square based on its row and column values. Because the same piece may be placed in multiple columns within the same row (as in the row of pawns, for example), place( ) will take as parameters a piece, a row, and a list of columns in which to place the piece.

sub place {
    # The first two parameters are the two-letter name of the piece and 
    # the row in which to place it. The piece can be placed in multiple 
    # columns, so the remainder is a list of columns...
    # 
    my ($piece, $y) = (shift @_, shift @_);
    
    open TILEFILE, $piece.".gif";
    my $tile = newFromGif GD::Image(\*TILEFILE);
    $tile->transparent($tile->getPixel(0,0));
 
    foreach my $x (@_) { 
        # First draw the appropriate square color
        #
        $image->filledRectangle($x*$tilew,$y*$tileh,  
                                ($x+1)*$tilew-1, ($y+1)*$tileh-1,
                                $fillcolors[tilecolor($x, $y)] );
        # Then copy the piece to the square
        #
        $image->copyResized($tile, $x * $tilew, $y * $tileh,
                            0, 0, $tilew, $tileh, 40,40);
    }
    close TILEFILE;
}

The tilecolor( ) subroutine determines the color of a square. The algorithm is simple; if the sum of the row and column is odd, the square is white.

sub tilecolor {
   # This routine takes a row and a column as a parameter
   # and returns 0 if that square is white, 1 if black
   #
   return abs((($_[0] + $_[1]) % 2)-1); 
}

Finally, the move( ) subroutine actually moves a piece from one square to another using GD’s copy( ) method. It doesn’t check whether the piece is actually in the square indicated by the startrow and startcol variables, so it can possibly “move” an empty square to another square.

sub move {
    # Parameters to move are in the order: startrow, startcol, endrow, endcol
    #
    my ($srow, $scol, $erow, $ecol) = @_;
    my ($startx, $endx) = ($scol * $tilew, $ecol * $tilew);
    my ($starty, $endy) = ($srow * $tileh, $erow * $tileh);

    my $startfill = $fillcolors[tilecolor($srow, $scol)]; 
    my $endfill = $fillcolors[tilecolor($erow, $ecol)]; 
    
    $image->fill($startx+1, $starty+1, $endfill);
    $image->copy($image, $endx, $endy, 
                 $startx, $starty, 
                 $tilew, $tileh );
    $image->filledRectangle($startx, $starty, 
                            $startx+$tilew-1, $starty+$tileh-1,
                            $startfill);
}
The board at the beginning of Move 4, Round 3 in the 1997 Kasparov/Deep Blue rematch

Figure 4-3. The board at the beginning of Move 4, Round 3 in the 1997 Kasparov/Deep Blue rematch

Figure 4.3 shows the board after several moves have been made.

Get Programming Web Graphics with Perl and GNU Softwar now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.