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.
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.
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); }
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.