<?php if (!defined('PmWiki')) exit(); /* Copyright 2005-2009 Patrick R. Michaud (pmichaud@pobox.com) This file is chess.php; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This recipe enables PGN (Portable Game Notation) chess markup in PmWiki. Strings in the markup text matching the PGN move format (e.g., things like "5. O-O Nf6" and "4...exd4") are internally recorded as moves in a "game", and then the {$FEN} markup can be used to obtain the current board position in Forsyth-Edwards Notation. The markup also allows lines of play to be explored, as (re-)specifying an earlier move automatically undoes any previously recorded later moves for the current game. The recipe also defines a "Chessboard:" InterMap shortcut, which can be used in combination with {$FEN} to draw a graphical representation of the current board position. Normally this is done with either the "(:chessboard:") markup, which generates a table, or the "Chessboard:{$FEN}&t=.gif" markup, which calls the chessboard.php application. An administrator can override the Chessboard: InterMap shortcut to change the default board display settings or use a different script entirely: # (in local/localmap.txt) # redefine "Chessboard:" to use a green+tan board Chessboard $PubDirUrl/chess/chessboard.php?light=ffffcc&dark=00bb00&fen=$1 # define "CB:" as a small 160x160 board CB $PubDirUrl/chess/chessboard.php?w=160&h=160&fen=$1 */ SDV($RecipeInfo['ChessMarkup']['Version'], '2009-01-22'); ## First, we define the Chessboard: InterMap shortcut SDV($LinkFunctions['Chessboard:'], 'LinkIMap'); SDV($IMap['Chessboard:'], "$PubDirUrl/chess/chessboard.php?fen=\$1"); ## Define the pgn and {$FEN} markups: $PGNMovePattern = "[KQRBNP]?[a-h]?[1-8]?x?[a-h][1-8](?:=[KQRBNP])?|O-O-O|O-O"; Markup('pgn', 'directives', "/(\\d+)\\.\\s*($PGNMovePattern|\\.+)(?:[\\s+#?!]*($PGNMovePattern))?/e", "PZZ(PGNMove($1, '$2', '$3')).Keep('$0')"); Markup('{$FEN}', '>pgn', '/\\{\\$FEN\\}/e', 'FENBoard()'); ## The (:chessboard:) markup by default generates a table, since ## some PHP installations don't have GD support compiled in. Markup('chessboard', '>{$FEN}', '/\\(:chessboard(\\s.*?)?:\\)/ei', "ChessTable(\$pagename, ParseArgs(PSS('$1')))"); ## $PGNMoves is an array of all moves (in PGN notation). The ## PGNMove($num, $white, $black) function adds moves for white and ## black into the array, removing any later moves that might be ## already present. $PGNMoves = array(); function PGNMove($num, $white, $black) { global $PGNMoves; if ($white{0} != '.') { $PGNMoves[$num*2-2] = $white; array_splice($PGNMoves, $num*2-1); } if ($black) { $PGNMoves[$num*2-1] = $black; array_splice($PGNMoves, $num*2); } } ## FENBoard() plays out the moves given in $PGNMoves and returns ## the resulting position as a FEN record. Internally $board ## is maintained as a 10x10 character string, with $board{11} ## corresponding to a8 and $board{88} corresponding to h1. ## Since PGN generally gives us only the name of the piece being ## moved and the square it moved to, we have to figure out where ## that piece came from. Castling is handled by directly manipulating ## the $board to the result of the castle. The from position ## for pawn moves is computed directly by inspection, while all ## other pieces use the $legalmoves array to determine the relative ## square offsets where pieces could've come from. Once we ## figure out where the piece came from, we put the piece in its ## new position ($to) and change the old position ($from) to an ## empty square. function FENBoard() { global $PGNMoves; $num = count($PGNMoves) * 2; # initialize the board and our allowed moves $board = "-----------rnbqkbnr--pppppppp--........--........-" . "-........--........--PPPPPPPP--RNBQKBNR-----------"; $legalmoves = array( 'K' => array(-11, -10, -9, -1, 1, 9, 10, 11), 'Q' => array(-11, -10, -9, -1, 1, 9, 10, 11), 'B' => array(-11, -9, 9, 11), 'R' => array(-10, -1, 1, 10), 'N' => array(-21, -19, -12, -8, 8, 12, 19, 21)); # now, walk through each move and update the board accordingly for($i=0; $i<$num; $i++) { $move = @$PGNMoves[$i]; # odd numbered $isblack = $i % 2; # moves are black if ($move == 'O-O') { # kingside castling $board = ($isblack) ? substr_replace($board, '.rk.', 15, 4) : substr_replace($board, '.RK.', 85, 4); continue; } if ($move == 'O-O-O') { # queenside castling $board = ($isblack) ? substr_replace($board, '..kr.', 11, 5) : substr_replace($board, '..KR.', 81, 5); continue; } if (preg_match( # all other moves "/^([KQRBNP]?)([a-h]?)([1-8]?)(x?)([a-h])([1-8])(=[KQRBNP])?/", $move, $match)) { @list($m, $piece, $ff, $fr, $cap, $tf, $tr, $promotion) = $match; $tf = strpos("abcdefgh", $tf)+1; $ff = ($ff) ? strpos("abcdefgh", $ff)+1 : 0; $to = (9-$tr)*10 + $tf; if (!$piece) $piece = "P"; $pp = ($isblack) ? strtolower($piece) : $piece; if ($pp == 'P') { # white's pawn move if ($cap) { # capture $from = (9-$tr)*10+10+$ff; if ($board{$to}=='.') $board{$to+10}='.'; # en passant } elseif ($board{$to+10}==$pp) $from=$to+10; # move elseif ($tr==4 && $board{$to+20}==$pp) $from=$to+20; # first move } elseif ($pp == 'p') { # black's pawn if ($cap) { # capture $from = (9-$tr)*10-10+$ff; if ($board{$to}=='.') $board{$to-10}='.'; # en passant } elseif ($board{$to-10}==$pp) $from=$to-10; # move elseif ($tr==5 && $board{$to-20}==$pp) $from=$to-20; # first move } else { # Here we look at squares along the lines for the piece # being moved. $n contains an offset for each square along # a valid line for the current piece. foreach($legalmoves[$piece] as $n) { for($from=$to+$n; $from>10 && $from<89; $from+=$n) { # if we find the piece we're looking for, we're done if ($board{$from} == $pp && (!$ff || ($from%10==$ff)) && (!$fr || ((int)($from/10)==(9-$fr)))) break 2; # if we find anything but an empty square, try another line if ($board{$from} != '.') continue 2; # kings and knights don't repeat offsets if ($piece == 'K' || $piece == 'N') continue 2; } } } # pawn promotions if ($promotion) $pp = ($isblack) ? strtolower($promotion{1}) : $promotion{1}; # move the piece $board{$to} = $pp; $board{$from} = '.'; } } # now, convert the board to a FEN record $board = preg_replace(array('/-+/', '/\\.+/e'), array('/', "strlen('$0')"), substr($board, 11, 78)); # and return it return $board; } ## The ChessTable function takes a FEN string (or the current ## game position as returned by FENBoard() above) and creates an ## HTML table representing the current game position. $ChessPieces = array( 'k' => 'kd-60.png', 'q' => 'qd-60.png', 'r' => 'rd-60.png', 'b' => 'bd-60.png', 'n' => 'nd-60.png', 'p' => 'pd-60.png', 'K' => 'kl-60.png', 'Q' => 'ql-60.png', 'R' => 'rl-60.png', 'B' => 'bl-60.png', 'N' => 'nl-60.png', 'P' => 'pl-60.png', '.' => 'blank-60.png'); SDV($HTMLStylesFmt['chess'], " table.chesstable td.square1 { background-color: #cccccc; } table.chesstable { border:1px solid #666666; } "); function ChessTable($pagename, $args) { global $ChessPieces, $ChessPubDirUrlFmt, $FmtV; SDV($ChessSquareFmt, "<td class='square\$SquareColor'><img src='\$FarmPubDirUrl/chess/\$PieceImg' alt='\$PieceName' width='\$SquareWidth' height='\$SquareHeight' /></td>"); SDV($ChessDefaults, array( 'width' => 240, 'class' => 'chesstable', 'cellspacing' => 0, 'cellpadding' => 0)); $args = array_merge($ChessDefaults, $args); $fen = (@$args['']) ? $args[''][0] : FENBoard(); $width = $args['width']; $height = (@$args['height'] > 0) ? $args['height'] : $width; $tableargs = ""; foreach(array('class', 'cellspacing', 'cellpadding', 'align', 'style') as $a) if (isset($args[$a])) $tableargs .= " $a='".str_replace("'", "'", $args[$a])."'"; ## build the 8x8 board from the FEN record $board = str_repeat('.', 64); $file = 0; $rank = 0; $len = strlen($fen); for($i=0; ($i<$len) && ($rank+$file<64); $i++) { $k = $fen{$i}; if ($k > 0) { $file += $k; continue; } if ($k == '/') { $rank += 8; $file = 0; continue; } if ($ChessPieces[$k]) { $board{$rank+$file} = $k; $file++; } } ## Now generate the table from the 8x8 board $FmtV['$SquareWidth'] = $width / 8; $FmtV['$SquareHeight'] = $height / 8; for($i=0; $i<64; $i++) { if ($i%8 == 0) $out[] = "<tr>"; $FmtV['$PieceImg'] = $ChessPieces[$board{$i}]; $FmtV['$PieceName'] = $board{$i}; $FmtV['$SquareColor'] = ($i + (int)($i/8)) % 2; $out[] = FmtPageName($ChessSquareFmt, $pagename); if ($i%8 == 7) $out[] = "</tr>"; } return "<table $tableargs>".Keep(implode('', $out))."</table>"; }