<?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("'", "&#039;", $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>";
}