/*
 *  pgn4web javascript chessboard
 *  copyright (C) 2009, 2011 Paolo Casaschi
 *  see README file and http://pgn4web.casaschi.net
 *  for credits, license and more details
 */

// automove. last move
// invalid moves in lon.pgn

var pgn4web_version = '2.16_OC.01';

var pgn4web_project_url = 'http://pgn4web.casaschi.net';
var pgn4web_project_author = 'Paolo Casaschi';
// pgn4web_project_email could be preassigned in pgn4web-server-config.js
var pgn4web_project_email;
if (pgn4web_project_email === undefined) { pgn4web_project_email = 'pgn4web@casaschi.net'; }

var helpWin=null;
function displayHelp(section){
  if (!section) { section = "top"; }
  if (helpWin && !helpWin.closed) { helpWin.close(); }
  helpWin = window.open(detectHelpLocation() + "?" + 
   (Math.floor(900 * Math.random()) + 100) + "#" + section, "pgn4web_help",
   "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
  if ((helpWin !== null) && (window.focus)) { helpWin.window.focus(); }
}


// custom functions executed at the given moments
// to be redefined in the HTML AFTER loading pgn4web.js

function customFunctionOnPgnTextLoad() {}
function customFunctionOnPgnGameLoad() {}
function customFunctionOnMove() {}
function customFunctionOnAlert(msg) {}

// API to parse custom header tags in customFunctionOnPgnGameLoad()

function customPgnHeaderTag(customTagString, htmlElementIdString, gameNum) {
  customTagString = customTagString.replace(/\W+/g, "");
  if (gameNum === undefined) { gameNum = currentGame; }
  if (tagValues = pgnGame[gameNum].match('\\[\\s*' + customTagString + '\\s*\"([^\"]+)\"\\s*\\]')) {
    tagValue = tagValues[1];
  } else { tagValue = ""; }
  if ((htmlElementIdString) && (theObject = document.getElementById(htmlElementIdString)) && (theObject.innerHTML !== null)) {
    theObject.innerHTML = tagValue;
  }
  return tagValue;
}

// API to parse custom comment tags in customFunctionOnMove()

function customPgnCommentTag(customTagString, htmlElementIdString, plyNum) {
  customTagString = customTagString.replace(/\W+/g, "");
  if (plyNum === undefined) { plyNum = CurrentPly; }
  if (tagValues = CurrentLine[plyNum].comment.match('\\[%' + customTagString + '\\s*([^\\]]+)\\s*\\]')) {
    tagValue = tagValues[1];
  } else { tagValue = ""; }
  if ((htmlElementIdString) && (theObject = document.getElementById(htmlElementIdString)) && (theObject.innerHTML !== null)) {
    theObject.innerHTML = tagValue;
  }
  return tagValue;
}

function strippedMoveComment(plyNum) {
  if (plyNum === undefined || !CurrentLine[plyNum] || !CurrentLine[plyNum].comment) { return ""; }
  return CurrentLine[plyNum].comment.replace(/\[%[^\]]*\]\s*/g,'').replace(/^\s+$/,'');
}

window.onload = start_pgn4web;

document.onkeydown = handlekey;

function start_pgn4web() {
  // keep startup logs at first run
  // reset alert log when reloading start_pgn4web
  if (alertFirstResetLoadingPgn) { alertFirstResetLoadingPgn = false; }
  else { resetAlert(); }
  InitImages(); 
  createBoard();
  if (LiveBroadcastDelay > 0) { restartLiveBroadcastTimeout(); }
}

var alertLog;
var alertLast;
var alertNum;
var alertNumSinceReset;
var fatalErrorNumSinceReset;
var alertPromptInterval = null;
var alertPromptOn = false;
var alertFirstResetLoadingPgn = true;

resetAlert();

function resetAlert() {
  alertLog = new Array(100);
  alertLast = alertLog.length - 1;
  alertNum = alertNumSinceReset = fatalErrorNumSinceReset = 0;
  stopAlertPrompt();
  if (!alertFirstResetLoadingPgn) {
    boardShortcut(debugShortcutSquare, "pgn4web v" + pgn4web_version + " debug info");
  }
}

function myAlert(msg, fatalError) {
  alertNum++;
  alertNumSinceReset++;
  if (fatalError) { fatalErrorNumSinceReset++; }
  alertLast = (alertLast + 1) % alertLog.length;
  alertLog[alertLast] = msg;
  boardShortcut(debugShortcutSquare, 
    "pgn4web v" + pgn4web_version + " debug info, " + alertNum + " alert" + (alertNum > 1 ? "s" : "")); 

  if ((LiveBroadcastDelay === 0) || (LiveBroadcastAlert === true)) {
    startAlertPrompt();
  }
  customFunctionOnAlert(msg);
}

function startAlertPrompt() {
  if (alertPromptOn) { return; } // dont start flashing twice
  if (alertPromptInterval) { clearTimeout(alertPromptInterval); }
  alertPromptInterval = setTimeout("alertPromptTick(true);", 500);
}

function stopAlertPrompt() {
  if (alertPromptInterval) { 
    clearTimeout(alertPromptInterval); 
    alertPromptInterval = null;
  }
  if (alertPromptOn) { alertPromptTick(false); }
}

function alertPromptTick(restart) {
  if (alertPromptInterval) { 
    clearTimeout(alertPromptInterval); 
    alertPromptInterval = null;
  }
  theObject = document.getElementById('tcol0trow0');
  if(theObject) {
    if (alertPromptOn) {
      if ((highlightOption) && 
        ((lastColFromHighlighted === 0 && lastRowFromHighlighted === 7) || 
        (lastColToHighlighted === 0 && lastRowToHighlighted === 7))) {
          theObject.className = 'highlightWhiteSquare';
        } else { theObject.className = 'whiteSquare'; }
    } else { theObject.className = 'blackSquare'; }

    alertPromptOn = !alertPromptOn;
    if (alertPromptOn) { alertPromptDelay = 500; }
    else { alertPromptDelay = 3000; }
  } else { alertPromptDelay = 1500; } // for alerts before the baord is drawn
  if (restart) { alertPromptInterval = setTimeout("alertPromptTick(true);", alertPromptDelay); }
}


function stopKeyProp(e) {
  e.cancelBubble = true;
  if (e.stopPropagation) { e.stopPropagation(); }
  if (e.preventDefault) { e.preventDefault(); }
  return false;
}

// for onFocus and onBlur actions on textboxes, allowing text typing
var shortcutKeysWereEnabled = false;
function disableShortcutKeysAndStoreStatus() {
  if ((shortcutKeysWereEnabled = shortcutKeysEnabled) === true) {
    SetShortcutKeysEnabled(false);
  }
}

function restoreShortcutKeysStatus() {
  if (shortcutKeysWereEnabled === true) { SetShortcutKeysEnabled(true); }
  shortcutKeysWereEnabled = false;
}

function customShortcutKey_Shift_0() {}
function customShortcutKey_Shift_1() {}
function customShortcutKey_Shift_2() {}
function customShortcutKey_Shift_3() {}
function customShortcutKey_Shift_4() {}
function customShortcutKey_Shift_5() {}
function customShortcutKey_Shift_6() {}
function customShortcutKey_Shift_7() {}
function customShortcutKey_Shift_8() {}
function customShortcutKey_Shift_9() {}

var shortcutKeysEnabled = false;
function handlekey(e) { 
  var keycode;
  var ct;

  if (!e) { e = window.event; }

  keycode = e.keyCode;

  if (e.altKey || e.ctrlKey || e.metaKey) { return true; }

  // escape always enabled: help and toggle shortcut keys
  if ((keycode != 27) && (shortcutKeysEnabled === false)) { return true; }

  switch(keycode) {

    case  8: // backspace
    case  9: // tab
    case 16: // shift
    case 17: // ctrl
    case 18: // alt
    case 32: // space
    case 33: // page-up
    case 34: // page-down
    case 35: // end
    case 36: // home
    case 45: // insert
    case 46: // delete
    case 92: // super
    case 93: // menu
      return true;

    case 27: // escape
      if (e.shiftKey) { interactivelyToggleShortcutKeys(); }
      else { displayHelp(); }
      return stopKeyProp(e);

    case 90: // z
      if (e.shiftKey) { window.open(pgn4web_project_url); }
      else { displayDebugInfo(); }
      return stopKeyProp(e);

    case 37: // left-arrow  
    case 74: // j
      MoveBackward(1);
      return stopKeyProp(e);

    case 38: // up-arrow
    case 72: // h
      GoToMove(StartPly);
      return stopKeyProp(e);

    case 39: // right-arrow
    case 75: // k
      MoveForward(1);
      return stopKeyProp(e);

    case 40: // down-arrow
    case 76: // l
      var ply = CurrentPly-1;
      if (CurrentLine[ply] && CurrentLine[ply].varcount()) {
        // go to var 1
        GoToMoveInLine(CurrentPly, CurrentLine[ply].getvar(0).UUID);
      } else if (CurrentLine.parentVar && CurrentLine.parentVar[ply] && (ct = CurrentLine.parentVar[ply].varcount())) {
        for (var i = 0; i < ct; i++) {
          var aVar = CurrentLine.parentVar[ply].getvar(i);
          if (aVar === CurrentLine) {
            // cycle through variations
            var cyc = (i+1)%(ct+1);
            if (cyc == ct) {
              // go to main line
              GoToMoveInLine(CurrentPly, CurrentLine.parentVar.UUID);
            } else {
              GoToMoveInLine(CurrentPly, CurrentLine.parentVar[ply].getvar((i+1)%ct).UUID);
            }
            break;
          }
        }
      } else {
        GoToMove(StartPly + PlyNumber);
      }
      return stopKeyProp(e);

    case 85: // u
      MoveToPrevComment();
      return stopKeyProp(e);

    case 73: // i
      MoveToNextComment();
      return stopKeyProp(e);

    case 83: // s
      searchPgnGamePrompt();
      return stopKeyProp(e);

    case 13: // enter
      if (e.shiftKey) { searchPgnGame(lastSearchPgnExpression, true); }
      else { searchPgnGame(lastSearchPgnExpression); }
      return stopKeyProp(e);

    case 65: // a
      MoveForward(1);
      SetAutoPlay(true);
      return stopKeyProp(e);

    case 48: // 0
      if (e.shiftKey) { customShortcutKey_Shift_0(); }
      else { SetAutoPlay(false); }
      return stopKeyProp(e);

    case 49: // 1
      if (e.shiftKey) { customShortcutKey_Shift_1(); }
      else { SetAutoplayDelayAndStart( 1*1000); }
      return stopKeyProp(e);

    case 50: // 2
      if (e.shiftKey) { customShortcutKey_Shift_2(); }
      else { SetAutoplayDelayAndStart( 2*1000); }
      return stopKeyProp(e);

    case 51: // 3
      if (e.shiftKey) { customShortcutKey_Shift_3(); }
      else { SetAutoplayDelayAndStart( 3*1000); }
      return stopKeyProp(e);

    case 52: // 4
      if (e.shiftKey) { customShortcutKey_Shift_4(); }
      else { SetAutoplayDelayAndStart( 4*1000); }
      return stopKeyProp(e);

    case 53: // 5
      if (e.shiftKey) { customShortcutKey_Shift_5(); }
      else { SetAutoplayDelayAndStart( 5*1000); }
      return stopKeyProp(e);

    case 54: // 6
      if (e.shiftKey) { customShortcutKey_Shift_6(); }
      else { SetAutoplayDelayAndStart( 6*1000); }
      return stopKeyProp(e);

    case 55: // 7
      if (e.shiftKey) { customShortcutKey_Shift_7(); }
      else { SetAutoplayDelayAndStart( 7*1000); }
      return stopKeyProp(e);

    case 56: // 8
      if (e.shiftKey) { customShortcutKey_Shift_8(); }
      else { SetAutoplayDelayAndStart( 8*1000); }
      return stopKeyProp(e);

    case 57: // 9
      if (e.shiftKey) { customShortcutKey_Shift_9(); }
      else { SetAutoplayDelayAndStart( 9*1000); }
      return stopKeyProp(e);

    case 81: // q
      SetAutoplayDelayAndStart(10*1000);
      return stopKeyProp(e);

    case 87: // w
      SetAutoplayDelayAndStart(20*1000);
      return stopKeyProp(e);

    case 69: // e
      SetAutoplayDelayAndStart(30*1000);
      return stopKeyProp(e);

    case 82: // r
      pauseLiveBroadcast();
      return stopKeyProp(e);

    case 84: // t
      if (e.shiftKey) { LiveBroadcastSteppingMode = !LiveBroadcastSteppingMode; }
      else { refreshPgnSource(); }
      return stopKeyProp(e);

    case 89: // y
      resumeLiveBroadcast();
      return stopKeyProp(e);

    case 70: // f
      FlipBoard();
      return stopKeyProp(e);

    case 71: // g
      SetHighlight(!highlightOption);
      return stopKeyProp(e);

    case 68: // d
      if (IsRotated) { FlipBoard(); }
      return stopKeyProp(e);

    case 88: // x
      if (numberOfGames > 1) {
        Init(Math.floor(Math.random()*numberOfGames));
        GoToMove(StartPly + Math.floor(Math.random()*(StartPly + PlyNumber)));
      }
      return stopKeyProp(e);

    case 67: // c
      if (numberOfGames > 1) { Init(Math.floor(Math.random()*numberOfGames)); }
      return stopKeyProp(e);

    case 86: // v
      if (numberOfGames > 1) { Init(0); }
      return stopKeyProp(e);

    case 66: // b
      Init(currentGame - 1);
      return stopKeyProp(e);

    case 78: // n
      Init(currentGame + 1);
      return stopKeyProp(e);

    case 77: // m
      if (numberOfGames > 1) { Init(numberOfGames - 1); }
      return stopKeyProp(e);

    case 79: // o
      SetCommentsOnSeparateLines(!commentsOnSeparateLines);
      oldPly = CurrentPly;
      Init();
      GoToMove(oldPly);
      return stopKeyProp(e);

    case 80: // p
      SetCommentsIntoMoveText(!commentsIntoMoveText);
      oldPly = CurrentPly;
      Init();
      GoToMove(oldPly);
      return stopKeyProp(e);

    case 189: // -
      SetShowVariations(!showVariations);
      PrintHTML();
      RefreshBoard();
      return stopKeyProp(e);

    default:
      return true;
  }
  return true;
}

boardOnClick = new Array(8);
boardTitle = new Array(8);
for (col=0; col<8; col++) {
  boardOnClick[col] = new Array(8);
  boardTitle[col] = new Array(8);
}
clearShortcutSquares("ABCDEFGH", "12345678");

function clearShortcutSquares(cols, rows) {
  if ((typeof cols != "string") || (typeof rows != "string")) { return; }
  for (c=0; c<cols.length; c++) { for (r=0; r<rows.length; r++) { 
      boardShortcut(cols.charAt(c).toUpperCase()+rows.charAt(r), "", function(){});
  } }
}

function boardShortcut(square, title, functionPointer) {
  if (square.charCodeAt === null) { return; }
  var col = square.charCodeAt(0) - 65; // 65="A"
  if ((col < 0) || (col > 7)) { return; }
  var row = 56 - square.charCodeAt(1); // 56="8"
  if ((row < 0) || (row > 7)) { return; }
  boardTitle[col][row] = title;
  if (functionPointer) { boardOnClick[col][row] = functionPointer; }
  if (theObject = document.getElementById('link_tcol' + col + 'trow' + row)) {
    if (IsRotated) { square = String.fromCharCode(72-col,49+row); }
    if (boardTitle[col][row] !== '') { squareTitle = square + ': ' + boardTitle[col][row]; }
    else { squareTitle = square; } 
    theObject.title = squareTitle;
  }
}

// PLEASE NOTE: 'boardShortcut' ALWAYS ASSUMES 'square' WITH WHITE ON BOTTOM

debugShortcutSquare = "A8";
// A8
boardShortcut("A8", "pgn4web v" + pgn4web_version + " debug info", function(){ displayDebugInfo(); });
// B8
boardShortcut("B8", "show this position FEN string", function(){ displayFenData(); });
// C8
boardShortcut("C8", "show this game PGN source data", function(){ displayPgnData(false); });
// D8
boardShortcut("D8", "show full PGN source data", function(){ displayPgnData(true); });
// E8
boardShortcut("E8", "search help", function(){ displayHelp("search"); });
// F8
boardShortcut("F8", "shortcut keys help", function(){ displayHelp("keys"); });
// G8
boardShortcut("G8", "shortcut squares help", function(){ displayHelp("squares"); });
// H8
boardShortcut("H8", "pgn4web help", function(){ displayHelp(); });
// A7
boardShortcut("A7", "pgn4web website", function(){ window.open(pgn4web_project_url); });
// B7
boardShortcut("B7", "toggle show comments in game text", function(){ SetCommentsIntoMoveText(!commentsIntoMoveText); oldPly = CurrentPly; Init(); GoToMove(oldPly); });
// C7
boardShortcut("C7", "toggle show comments on separate lines in game text", function(){ SetCommentsOnSeparateLines(!commentsOnSeparateLines); oldPly = CurrentPly; Init(); GoToMove(oldPly); });
// D7
boardShortcut("D7", "toggle highlight last move", function(){ SetHighlight(!highlightOption); });
// E7
boardShortcut("E7", "flip board", function(){ FlipBoard(); });
// F7
boardShortcut("F7", "show white on bottom", function(){ if (IsRotated) { FlipBoard(); } });
// G7
boardShortcut("G7", "toggle autoplay next game", function(){ SetAutoplayNextGame(!autoplayNextGame); });
// H7
boardShortcut("H7", "toggle enabling shortcut keys", function(){ interactivelyToggleShortcutKeys(); });
// A6
boardShortcut("A6", "pause live broadcast automatic refresh", function(){ pauseLiveBroadcast(); });
// B6
boardShortcut("B6", "restart live broadcast automatic refresh", function(){ restartLiveBroadcast(); });
// C6
boardShortcut("C6", "search previous finished game", function(){ searchPgnGame('\\[\\s*Result\\s*"(?!\\*"\\s*\\])', true); });
// D6
boardShortcut("D6", "search previous unfinished game", function(){ searchPgnGame('\\[\\s*Result\\s*"\\*"\\s*\\]', true); });
// E6
boardShortcut("E6", "search next unfinished game", function(){  searchPgnGame('\\[\\s*Result\\s*"\\*"\\s*\\]', false); });
// F6
boardShortcut("F6", "search next finished game", function(){ searchPgnGame('\\[\\s*Result\\s*"(?!\\*"\\s*\\])', false); });
// G6
boardShortcut("G6", "toggle live broadcast stepping", function(){ LiveBroadcastSteppingMode = !LiveBroadcastSteppingMode; });
// H6
boardShortcut("H6", "force games refresh during live broadcast", function(){ refreshPgnSource(); });
// A5
boardShortcut("A5", "repeat last search backward", function(){ searchPgnGame(lastSearchPgnExpression, true); });
// B5
boardShortcut("B5", "search prompt", function(){ searchPgnGamePrompt(); });
// C5
boardShortcut("C5", "repeat last search", function(){ searchPgnGame(lastSearchPgnExpression); });
// D5
boardShortcut("D5", "search previous win result", function(){ searchPgnGame('\\[\\s*Result\\s*"(1-0|0-1)"\\s*\\]', true); });
// E5
boardShortcut("E5", "search next win result", function(){ searchPgnGame('\\[\\s*Result\\s*"(1-0|0-1)"\\s*\\]', false); });
// F5
boardShortcut("F5", "", function(){});
// G5
boardShortcut("G5", "", function(){});
// H5
boardShortcut("H5", "", function(){});
// A4
boardShortcut("A4", "search previous event", function(){ searchPgnGame('\\[\\s*Event\\s*"(?!' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\])', true); });
// B4
boardShortcut("B4", "search previous round of same event", function(){ searchPgnGame('\\[\\s*Event\\s*"' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\].*\\[\\s*Round\\s*"(?!' + fixRegExp(gameRound[currentGame]) + '"\\s*\\])|\\[\\s*Event\\s*"' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\].*\\[\\s*Round\\s*"(?!' + fixRegExp(gameRound[currentGame]) + '"\\s*\\])', true); });
// C4
boardShortcut("C4", "search previous game of same black player", function(){ searchPgnGame('\\[\\s*Black\\s*"' + fixRegExp(gameBlack[currentGame]) + '"\\s*\\]', true); });
// D4
boardShortcut("D4", "search previous game of same white player", function(){ searchPgnGame('\\[\\s*White\\s*"' + fixRegExp(gameWhite[currentGame]) + '"\\s*\\]', true); });
// E4
boardShortcut("E4", "search next game of same white player", function(){ searchPgnGame('\\[\\s*White\\s*"' + fixRegExp(gameWhite[currentGame]) + '"\\s*\\]', false); });
// F4
boardShortcut("F4", "search next game of same black player", function(){  searchPgnGame('\\[\\s*Black\\s*"' + fixRegExp(gameBlack[currentGame]) + '"\\s*\\]', false); });
// G4
boardShortcut("G4", "search next round of same event", function(){ searchPgnGame('\\[\\s*Event\\s*"' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\].*\\[\\s*Round\\s*"(?!' + fixRegExp(gameRound[currentGame]) + '"\\s*\\])|\\[\\s*Event\\s*"' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\].*\\[\\s*Round\\s*"(?!' + fixRegExp(gameRound[currentGame]) + '"\\s*\\])', false); });
// H4
boardShortcut("H4", "search next event", function(){ searchPgnGame('\\[\\s*Event\\s*"(?!' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\])', false); });
// A3
boardShortcut("A3", "load first game", function(){ if (numberOfGames > 1) { Init(0); } });
// B3
boardShortcut("B3", "jump to previous games decile", function(){ if (currentGame > 0) { calculateDeciles(); for(ii=(deciles.length-2); ii>=0; ii--) { if (currentGame > deciles[ii]) { Init(deciles[ii]); break; } } } });
// C3
boardShortcut("C3", "load previous game", function(){ Init(currentGame - 1); });
// D3
boardShortcut("D3", "load random game", function(){ if (numberOfGames > 1) { Init(Math.floor(Math.random()*numberOfGames)); } });
// E3
boardShortcut("E3", "load random game at random position", function(){ Init(Math.floor(Math.random()*numberOfGames)); GoToMove(StartPly + Math.floor(Math.random()*(StartPly + PlyNumber))); });
// F3
boardShortcut("F3", "load next game", function(){ Init(currentGame + 1); });
// G3
boardShortcut("G3", "jump to next games decile", function(){ if (currentGame < numberOfGames - 1) { calculateDeciles(); for(ii=1; ii<deciles.length; ii++) { if (currentGame < deciles[ii]) { Init(deciles[ii]); break; } } } });
// H3
boardShortcut("H3", "load last game", function(){ if (numberOfGames > 1) { Init(numberOfGames - 1); } });
// A2
boardShortcut("A2", "stop autoplay", function(){ SetAutoPlay(false); });
// B2
boardShortcut("B2", "toggle autoplay", function(){ SwitchAutoPlay(); });
// C2
boardShortcut("C2", "autoplay 1 second", function(){ SetAutoplayDelayAndStart( 1*1000); });
// D2
boardShortcut("D2", "autoplay 2 seconds", function(){ SetAutoplayDelayAndStart( 2*1000); });
// E2
boardShortcut("E2", "autoplay 3 seconds", function(){ SetAutoplayDelayAndStart( 3*1000); });
// F2
boardShortcut("F2", "autoplay 5 seconds", function(){ SetAutoplayDelayAndStart( 5*1000); });
// G2
boardShortcut("G2", "autoplay 10 seconds", function(){ SetAutoplayDelayAndStart(10*1000); });
// H2
boardShortcut("H2", "autoplay 30 seconds", function(){ SetAutoplayDelayAndStart(30*1000); });
// A1
boardShortcut("A1", "go to game start", function(){ GoToMoveInLine(0, Moves.UUID); });
// B1
boardShortcut("B1", "go to previous comment", function(){ MoveToPrevComment(); });
// C1
boardShortcut("C1", "move 6 half-moves backward", function(){ MoveBackward(6); });
// D1
boardShortcut("D1", "move backward", function(){ MoveBackward(1); });
// E1
boardShortcut("E1", "move forward", function(){ MoveForward(1); });
// F1
boardShortcut("F1", "move 6 half-moves forward", function(){ MoveForward(6); });
// G1
boardShortcut("G1", "go to next comment", function(){ MoveToNextComment(); });
// H1
boardShortcut("H1", "go to game end", function(){ GoToMove(StartPly + PlyNumber); });


var deciles = new Array(11);
function calculateDeciles() {
  for (ii=0; ii<deciles.length; ii++) { 
    deciles[ii] = Math.round((numberOfGames - 1) * ii / (deciles.length - 1));
  }
}

function detectJavascriptLocation() {
  jspath = "";
  var e = document.getElementsByTagName('script');
  for(var i=0; i<e.length; i++) {
    if ((e[i].src) && (e[i].src.match(/(pgn4web|pgn4web-compacted)\.js/))) {
      jspath = e[i].src; 
    }
  }
  return jspath;
}

function detectHelpLocation() {
  return detectJavascriptLocation().replace(/(pgn4web|pgn4web-compacted)\.js/, "help.html"); 
}

function detectBaseLocation() {
  base = "";
  var e = document.getElementsByTagName('base');
  for(var i=0; i<e.length; i++) {
    if (e[i].href) { base = e[i].href; }
  }
  return base;
}


debugWin = null;
function displayDebugInfo() {
  stopAlertPrompt();
  debugInfo = 'pgn4web: version=' + pgn4web_version + ' homepage=' + pgn4web_project_url + '\n\n' +
    'HTMLURL: length=' + location.href.length + ' url=' +
    (location.href.length < 100 ? location.href : (location.href.substring(0,99) + '...')) + '\n' +
    'BASEURL: url=' + detectBaseLocation() + '\n' +
    'JSURL: url=' + detectJavascriptLocation() + '\n\n' +
    'PGNURL: url=' + pgnUrl + '\n' +
    'PGNTEXT: length=';
  if (document.getElementById("pgnText") !== null) { 
    debugInfo += document.getElementById("pgnText").tagName.toLowerCase() == "textarea" ?
      document.getElementById("pgnText").value.length :
      document.getElementById("pgnText").innerHTML.length +
      ' container=' + document.getElementById("pgnText").tagName.toLowerCase();
    // pgn4web up to 1.77 used <span> for pgnText
  }
  debugInfo += '\n\n' +
    'GAMES: current=' + (currentGame+1) + ' number=' + numberOfGames + '\n' +
    'PLY: start=' + StartPly + ' current=' + CurrentPly + ' number=' + PlyNumber + '\n' +
    'AUTOPLAY: ' + (isAutoPlayOn ? 'delay=' + Delay + 'ms' + ' autoplaynext=' + autoplayNextGame : 'off') +
    '\n\n' +
    'LIVEBROADCAST: ' + (LiveBroadcastDelay > 0 ? 'ticker=' + LiveBroadcastTicker + ' delay=' + LiveBroadcastDelay + 'm' + ' started=' + LiveBroadcastStarted + ' ended=' + LiveBroadcastEnded + ' paused=' + LiveBroadcastPaused + ' demo=' + LiveBroadcastDemo + ' alert=' + LiveBroadcastAlert + ' stepping=' + LiveBroadcastSteppingMode + '\n' + 'refreshed: ' + LiveBroadcastLastRefreshedLocal + '\n' + 'received: ' + LiveBroadcastLastReceivedLocal + '\n' + 'modified (server time): ' + LiveBroadcastLastModified_ServerTime() : 'off') + 
    '\n\n' +
    'ALERTLOG: fatalnew=' + fatalErrorNumSinceReset + ' new=' + alertNumSinceReset + 
    ' shown=' + Math.min(alertNum, alertLog.length) + ' total=' + alertNum + '\n--';
  if (alertNum > 0) {
    for (ii = 0; ii<alertLog.length; ii++) {
      if (alertLog[(alertNum - 1 - ii) % alertLog.length] === undefined) { break; }
      else { debugInfo += "\n" + alertLog[(alertNum - 1 - ii) % alertLog.length] + "\n--"; }
    }
  }
  if (confirm(debugInfo + '\n\nclick OK to show this debug info in a browser window for cut and paste')) {
    if (debugWin && !debugWin.closed) { debugWin.close(); }
    debugWin = window.open("", "debug_data",
      "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
    if (debugWin !== null) {
      text = "<html><head><title>pgn4web debug info</title>" +
        "<link rel='shortcut icon' href='pawn.ico' /></head>" +
        "<body>\n<pre>\n" + debugInfo + "\n</pre>\n</body></html>";
      debugWin.document.open("text/html", "replace");
      debugWin.document.write(text);
      debugWin.document.close();
      if (window.focus) { debugWin.window.focus(); }
    }
  }
  alertNumSinceReset = fatalErrorNumSinceReset = 0;
}

pgnWin = null;
function displayPgnData(allGames) {
  if (allGames === null) { allGames = true; }
  if (pgnWin && !pgnWin.closed) { pgnWin.close(); }
  pgnWin = window.open("", "pgn_data",
    "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
  if (pgnWin !== null) {
    text = "<html><head><title>pgn4web PGN source</title>" + 
      "<link rel='shortcut icon' href='pawn.ico' /></head><body>\n<pre>\n";
    if (allGames) { for (ii = 0; ii < numberOfGames; ++ii) { text += pgnGame[ii]; } }
    else { text += pgnGame[currentGame]; }
    text += "\n</pre>\n</body></html>";
    pgnWin.document.open("text/html", "replace");
    pgnWin.document.write(text);
    pgnWin.document.close();
    if (window.focus) { pgnWin.window.focus(); }
  }
}

function CurrentFEN(applyVar) {
  var currentFEN = "";

  var emptyCounterFen = 0;
  for (var row=7; row>=0; row--) {
    for (var col=0; col<=7; col++) {
      if (Board[col][row] === 0) { emptyCounterFen++; }
      else {
        if (emptyCounterFen > 0) {
          currentFEN += "" + emptyCounterFen;
          emptyCounterFen = 0;
        }
        if (Board[col][row] > 0) { currentFEN += FenPieceName.toUpperCase().charAt(Board[col][row]-1); }
        else if (Board[col][row] < 0) { currentFEN += FenPieceName.toLowerCase().charAt(-Board[col][row]-1); }
      }
    }
    if (emptyCounterFen > 0) {
      currentFEN += "" + emptyCounterFen;
      emptyCounterFen = 0;
    }
    if (row>0) { currentFEN += "/"; }
  }
 
  // active color
  currentFEN += CurrentPly%2 === 0 ? " w" : " b";

  // castling availability
  var CastlingShortFEN = new Array(2);
  CastlingShortFEN[0] = CurrentLine.CastlingShort[0];
  CastlingShortFEN[1] = CurrentLine.CastlingShort[1];
  var CastlingLongFEN = new Array(2);
  CastlingLongFEN[0] = CurrentLine.CastlingLong[0];
  CastlingLongFEN[1] = CurrentLine.CastlingLong[1];
  // TO DO this is a problem
  for (var thisPly = StartPly; thisPly < CurrentPly; thisPly++) {
    var SideToMoveFEN = thisPly%2;
    var BackrowSideToMoveFEN = SideToMoveFEN * 7;
    if (CurrentLine.HistType[0][thisPly] == 1) { 
      CastlingShortFEN[SideToMoveFEN] = CastlingLongFEN[SideToMoveFEN] = -1;
    }
    if ((CurrentLine.HistCol[0][thisPly] == CastlingShortFEN[SideToMoveFEN]) && 
      (CurrentLine.HistRow[0][thisPly] == BackrowSideToMoveFEN)) {
      CastlingShortFEN[SideToMoveFEN] = -1;
    }
    if ((CurrentLine.HistCol[0][thisPly] == CastlingLongFEN[SideToMoveFEN]) && 
      (CurrentLine.HistRow[0][thisPly] == BackrowSideToMoveFEN)) {
      CastlingLongFEN[SideToMoveFEN] = -1;
    }
  }

  var CastlingFEN = "";
  if (SquareOnBoard(CastlingShortFEN[0], 0)) {
    for (ii = 7; ii > CastlingShortFEN[0]; ii--) { if (Board[ii][0] == 3) { break; } }
    if (ii == CastlingShortFEN[0]) { CastlingFEN += FenPieceName.toUpperCase().charAt(0); } 
    else { CastlingFEN += columnsLetters.toUpperCase().charAt(CastlingShortFEN[0]); } 
  }
  if (SquareOnBoard(CastlingLongFEN[0], 0)) {
    for (ii = 0; ii < CastlingLongFEN[0]; ii++) { if (Board[ii][0] == 3) { break; } }
    if (ii == CastlingLongFEN[0]) { CastlingFEN += FenPieceName.toUpperCase().charAt(1); }
    else { CastlingFEN += columnsLetters.toUpperCase().charAt(CastlingLongFEN[0]); }
  }
  if (SquareOnBoard(CastlingShortFEN[1], 7)) {
    for (ii = 7; ii > CastlingShortFEN[1]; ii--) { if (Board[ii][7] == -3) { break; } }
    if (ii == CastlingShortFEN[1]) { CastlingFEN += FenPieceName.toLowerCase().charAt(0); }
    else { CastlingFEN += columnsLetters.toLowerCase().charAt(CastlingShortFEN[1]); }
  }
  if (SquareOnBoard(CastlingLongFEN[1], 7)) {
    for (ii = 0; ii < CastlingLongFEN[1]; ii++) { if (Board[ii][7] == -3) { break; } }
    if (ii == CastlingLongFEN[1]) { CastlingFEN += FenPieceName.toLowerCase().charAt(1); }
    else { CastlingFEN += columnsLetters.toLowerCase().charAt(CastlingLongFEN[1]); }
  }
  if (CastlingFEN === "") { CastlingFEN = "-"; }
  
  currentFEN += " " + CastlingFEN;
 
  // en-passant square
  if (CurrentLine.HistEnPassant[CurrentPly-1]) {
    currentFEN += " " + String.fromCharCode(CurrentLine.HistEnPassantCol[CurrentPly-1] + 97);
    currentFEN += CurrentPly%2 === 0 ? "6" : "3";
  } else { currentFEN += " -"; }

  // halfmove clock
  var HalfMoveClock = CurrentLine.InitialHalfMoveClock;  
  for (thisPly = StartPly; thisPly < CurrentPly; thisPly++) {
    if ((CurrentLine.HistType[0][thisPly] == 6) || (CurrentLine.HistPieceId[1][thisPly] >= 16)) { HalfMoveClock = 0; }
    else { HalfMoveClock++; } 
  }
  currentFEN += " " + HalfMoveClock;

  // fullmove number
  currentFEN += " " + (Math.floor(CurrentPly/2)+1);

  if (applyVar) { // hack
    applyVar.CastlingLong[0] = CastlingLongFEN[0];
    applyVar.CastlingLong[1] = CastlingLongFEN[1];
    applyVar.CastlingShort[0] = CastlingShortFEN[0];
    applyVar.CastlingShort[1] = CastlingShortFEN[1];
    applyVar.InitialHalfMoveClock = HalfMoveClock;
  }
  return currentFEN;
}

fenWin = null;
function displayFenData() {
  if (fenWin && !fenWin.closed) { fenWin.close(); }

  currentFEN = CurrentFEN();

  currentMovesString = "";
  lastLineStart = 0;
  for(var thisPly = CurrentPly; thisPly <= StartPly + PlyNumber; thisPly++) {
    if (!CurrentLine[thisPly]) continue;
    addToMovesString = "";
    if (thisPly == StartPly + PlyNumber) {
      if ((gameResult[currentGame]) && (gameResult[currentGame] != "*")) {
        addToMovesString = gameResult[currentGame];
      }
    } else {
      if ((thisPly%2) === 0) { addToMovesString = (Math.floor(thisPly/2)+1) + ". "; }
      else if (thisPly == CurrentPly) {
        addToMovesString = (Math.floor(thisPly/2)+1) + "... ";
      }
      addToMovesString += CurrentLine[thisPly].move;
    }
    if (currentMovesString.length + addToMovesString.length + 1 > lastLineStart + 80) {
      lastLineStart = currentMovesString.length;
      currentMovesString += "\n" + addToMovesString;
    } else {
      if (currentMovesString.length > 0) { currentMovesString += " "; }
      currentMovesString += addToMovesString;
    }
  }

  fenWin = window.open("", "fen_data", 
    "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
  if (fenWin !== null) {
    text = "<html>" +
      "<head><title>pgn4web FEN string</title><link rel='shortcut icon' href='pawn.ico' /></head>" +
      "<body>\n<b><pre>\n\n" + currentFEN + "\n\n</pre></b>\n<hr>\n<pre>\n\n" +
      "[Event \"" + (gameEvent[currentGame] ? gameEvent[currentGame] : "?") + "\"]\n" +
      "[Site \"" + (gameSite[currentGame] ? gameSite[currentGame] : "?") + "\"]\n" +
      "[Date \"" + (gameDate[currentGame] ? gameDate[currentGame] : "????.??.??") + "\"]\n" +
      "[Round \"" + (gameRound[currentGame] ? gameRound[currentGame] : "?") + "\"]\n" +
      "[White \"" + (gameWhite[currentGame] ? gameWhite[currentGame] : "?") + "\"]\n" +
      "[Black \"" + (gameBlack[currentGame] ? gameBlack[currentGame] : "?") + "\"]\n" +
      "[Result \"" + (gameResult[currentGame] ? gameResult[currentGame] : "*") + "\"]\n";
    if (currentFEN != FenStringStart) { 
      text += "[SetUp \"1\"]\n" + "[FEN \"" + CurrentFEN() + "\"]\n";
    }
    if (gameVariant[currentGame] !== "") { text += "[Variant \"" + gameVariant[currentGame] + "\"]\n"; }
    text += "\n" + currentMovesString + "\n</pre>\n</body></html>";
    fenWin.document.open("text/html", "replace");
    fenWin.document.write(text);
    fenWin.document.close();
    if (window.focus) { fenWin.window.focus(); }
  }
}


var pgnGame = new Array();
var numberOfGames = -1; 
var currentGame   = -1;

var firstStart = true;

var gameDate = new Array();
var gameWhite = new Array();
var gameBlack = new Array();
var gameEvent = new Array();
var gameSite = new Array();
var gameRound = new Array();
var gameResult = new Array();
var gameSetUp = new Array();
var gameFEN = new Array();
var gameInitialWhiteClock = new Array();
var gameInitialBlackClock = new Array();
var gameVariant = new Array();

var oldAnchor = '';

var isAutoPlayOn = false;
var AutoPlayInterval = null;
var Delay = 1000; // milliseconds
var autostartAutoplay = false;
var autoplayNextGame = false;

var initialGame = 1;
var initialHalfmove = 0;
var alwaysInitialHalfmove = false;

var LiveBroadcastInterval = null;
var LiveBroadcastDelay = 0; // minutes
var LiveBroadcastAlert = false;
var LiveBroadcastDemo = false;
var LiveBroadcastStarted = false;
var LiveBroadcastEnded = false;
var LiveBroadcastPaused = false;
var LiveBroadcastTicker = 0;
var LiveBroadcastStatusString = "";
var LiveBroadcastLastModified = new Date(0); // default to epoch start
var LiveBroadcastLastModifiedHeader = LiveBroadcastLastModified.toUTCString();
var LiveBroadcastLastReceivedLocal = 'unavailable';
var LiveBroadcastLastRefreshedLocal = 'unavailable';
var LiveBroadcastPlaceholderEvent = 'live chess broadcast';
var LiveBroadcastPlaceholderPgn = '[Event "' + LiveBroadcastPlaceholderEvent + '"]';
var gameDemoMaxPly = new Array();
var gameDemoLength = new Array();
var LiveBroadcastSteppingMode = false;

var castleRook = -1;
var mvCapture =  0;
var mvIsCastling =  0;
var mvIsPromotion =  0;
var mvFromCol = -1;
var mvFromRow = -1;
var mvToCol = -1;
var mvToRow = -1;
var mvPiece = -1;
var mvPieceId = -1;
var mvPieceOnTo = -1;
var mvCaptured = -1;
var mvCapturedId = -1;

Board = new Array(8);
for(i=0; i<8; ++i) { Board[i] = new Array(8); }

PieceCol = new Array(2);
PieceRow = new Array(2);
PieceType = new Array(2);
PieceMoveCounter = new Array(2);

for(i=0; i<2; ++i){
  PieceCol[i] = new Array(16);
  PieceRow[i] = new Array(16);
  PieceType[i] = new Array(16);
  PieceMoveCounter[i] = new Array(16);
}

var FenPieceName = "KQRBNP";
var PieceCode = new Array(); // IE needs an array to work with [index]
for (i=0; i<6; i++) { PieceCode[i] = FenPieceName.charAt(i); }
var FenStringStart = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
var columnsLetters = "ABCDEFGH";

startingSquareSize = -1;
startingImageSize = -1;

PiecePicture = new Array(2);
for(i=0; i<2; ++i) { PiecePicture[i] = new Array(6); }

var ImagePath = '';                                                 
var ImagePathOld = null;
var imageType = 'png';
var defaultImagesSize = 40;

var highlightOption = true;

var commentsIntoMoveText = true;
var commentsOnSeparateLines = false;
var showVariations = true;

var pgnUrl = '';

var LineLookup; 
var Moves; 
var CurrentLine; // I wish this didn't need to be global, but there you go

var MoveColor;
var MoveCount;
var PlyNumber;
var StartPly;
var CurrentPly;

var IsRotated = false;

var pgnHeaderTagRegExp       = /\[\s*(\w+)\s*"([^"]*)"\s*\]/; 
var pgnHeaderTagRegExpGlobal = /\[\s*(\w+)\s*"([^"]*)"\s*\]/g;
var dummyPgnHeader = '[x""]';
var emptyPgnHeader = '[Event ""]\n[Site ""]\n[Date ""]\n[Round ""]\n[White ""]\n[Black ""]\n[Result ""]\n\n';
var templatePgnHeader = '[Event "?"]\n[Site "?"]\n[Date "?"]\n[Round "?"]\n[White "?"]\n[Black "?"]\n[Result "?"]\n';
var alertPgnHeader = '[Event ""]\n[Site ""]\n[Date ""]\n[Round ""]\n[White ""]\n[Black ""]\n[Result ""]\n\n{error: click on the top left chessboard square for debug info}';

var gameSelectorHead = ' ...';
var gameSelectorMono = true;
var gameSelectorNum = false;
var gameSelectorNumLenght = 0;
var gameSelectorChEvent = 0;
var gameSelectorChSite = 0;
var gameSelectorChRound = 0;
var gameSelectorChWhite = 15;
var gameSelectorChBlack = 15;
var gameSelectorChResult = 0;
var gameSelectorChDate = 10;

function CheckLegality(what, plyCount) {
  var retVal;
  var start;
  var end;
  var isCheck;

  // castling move?
  if (what == 'O-O') {
    if (!CheckLegalityOO()) { return false; }
    start = PieceCol[MoveColor][0];
    end   = 6;
    while (start < end) {
      isCheck = IsCheck(start, MoveColor*7, MoveColor);
      if (isCheck) { return false; }
      ++start;
    }
    StoreMove(plyCount);
    return true;
  } else if (what == 'O-O-O') {
    if (!CheckLegalityOOO()) { return false; }
    start = PieceCol[MoveColor][0];
    end   = 2;
    while (start > end) {
      isCheck = IsCheck(start, MoveColor*7, MoveColor);
      if (isCheck) { return false; }
      --start;
    }
    StoreMove(plyCount);
    return true;
  } 
  
  // not a capture => square must be empty
  // capture => "square to" occupied by opposite color piece (except en-passant)
  // "square to" moved piece different from piece => pawn promotion 
  if (!mvCapture) {
    if (Board[mvToCol][mvToRow] !== 0) { return false; }
  }
  if ((mvCapture) && (Color(Board[mvToCol][mvToRow]) != 1-MoveColor)) {
    if ((mvPiece != 6) || (!CurrentLine.HistEnPassant[plyCount-1]) || (CurrentLine.HistEnPassantCol[plyCount-1] != mvToCol) ||
	(mvToRow != 5-3*MoveColor)) { return false; }
  }
  if (mvIsPromotion) {
    if (mvPiece     != 6) { return false; }
    if (mvPieceOnTo >= 6) { return false; }
    if (mvToRow     != 7*(1-MoveColor)) { return false; }
  }
  
  // piece move => loop over same type pieces: which could move there?
  var pieceId;
  for (pieceId = 0; pieceId < 16; ++pieceId) {
     if (PieceType[MoveColor][pieceId] == mvPiece) {
      if (mvPiece == 1) { retVal = CheckLegalityKing(pieceId); }
      else if (mvPiece == 2) { retVal = CheckLegalityQueen(pieceId); }
      else if (mvPiece == 3) { retVal = CheckLegalityRook(pieceId); }
      else if (mvPiece == 4) { retVal = CheckLegalityBishop(pieceId); }
      else if (mvPiece == 5) { retVal = CheckLegalityKnight(pieceId); }
      else if (mvPiece == 6) { retVal = CheckLegalityPawn(pieceId); }
      if (retVal) {
	mvPieceId = pieceId;
        // board updated: king check?
        StoreMove(plyCount);
        isCheck = IsCheck(PieceCol[MoveColor][0], PieceRow[MoveColor][0], MoveColor);
	if (!isCheck) { return true; }
	else { UndoMove(plyCount); }
      }
    }
  }
  return false;
}

function CheckLegalityKing(thisKing) {
  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisKing])) { return false; }
  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisKing])) { return false; }
  if (Math.abs(PieceCol[MoveColor][thisKing]-mvToCol) > 1) { return false; }
  if (Math.abs(PieceRow[MoveColor][thisKing]-mvToRow) > 1) { return false; }
  return true;
}

function CheckLegalityQueen(thisQueen) {
  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisQueen])) { return false; }
  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisQueen])) { return false; }
  if (((PieceCol[MoveColor][thisQueen]-mvToCol) *
    (PieceRow[MoveColor][thisQueen]-mvToRow) !== 0) &&
    (Math.abs(PieceCol[MoveColor][thisQueen]-mvToCol) !=
    Math.abs(PieceRow[MoveColor][thisQueen]-mvToRow)))
  { return false; }
  var clearWay = CheckClearWay(thisQueen);
  if (!clearWay) { return false; }
  return true;
}

function CheckLegalityRook(thisRook) {
  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisRook])) { return false; }
  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisRook])) { return false; }
  if ((PieceCol[MoveColor][thisRook]-mvToCol) *
    (PieceRow[MoveColor][thisRook]-mvToRow) !== 0)
  { return false; }
  var clearWay = CheckClearWay(thisRook);
  if (!clearWay) { return false; }
  return true;
}

function CheckLegalityBishop(thisBishop) {
  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisBishop])) { return false; }
  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisBishop])) { return false; }
  if (Math.abs(PieceCol[MoveColor][thisBishop]-mvToCol) !=
    Math.abs(PieceRow[MoveColor][thisBishop]-mvToRow))
  { return false; }
  var clearWay = CheckClearWay(thisBishop);
  if (!clearWay) { return false; }
  return true;
}

function CheckLegalityKnight(thisKnight) {
  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisKnight])) { return false; }
  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisKnight])) { return false; }
  if (Math.abs(PieceCol[MoveColor][thisKnight]-mvToCol) *
    Math.abs(PieceRow[MoveColor][thisKnight]-mvToRow) != 2)
  { return false; }
  return true;
}

function CheckLegalityPawn(thisPawn) {
  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisPawn])) { return false; }
  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisPawn])) { return false; }
  if (Math.abs(PieceCol[MoveColor][thisPawn]-mvToCol) != mvCapture) { return false; }
  if (mvCapture) {
    if (PieceRow[MoveColor][thisPawn]-mvToRow != 2*MoveColor-1) { return false; }
  } else {
    if (PieceRow[MoveColor][thisPawn]-mvToRow == 4*MoveColor-2){
      if (PieceRow[MoveColor][thisPawn] != 1+5*MoveColor) { return false; }
      if (Board[mvToCol][mvToRow+2*MoveColor-1] !== 0) { return false; }
    } else {
      if (PieceRow[MoveColor][thisPawn]-mvToRow != 2*MoveColor-1) { return false; }
    }
  }
  return true;
}

function CheckLegalityOO() {
  if (CurrentLine.CastlingShort[MoveColor] < 0) { return false; }
  if (PieceMoveCounter[MoveColor][0] > 0) { return false; }
  // which rook is castling
  var legal = false;
  var thisRook = 0;
  while (thisRook < 16) {
    if ((PieceCol[MoveColor][thisRook] == CurrentLine.CastlingShort[MoveColor]) &&
      (PieceCol[MoveColor][thisRook] > PieceCol[MoveColor][0]) &&
      (PieceRow[MoveColor][thisRook] == MoveColor*7) &&
      (PieceType[MoveColor][thisRook] == 3)) {
      legal = true;
      break;
    }
    ++thisRook;
  }
  if (!legal) { return false; }
  if (PieceMoveCounter[MoveColor][thisRook] > 0) { return false; }
  
  // check no piece between king and rook
  // clear king/rook squares for Chess960
  Board[PieceCol[MoveColor][0]][MoveColor*7] = 0;
  Board[PieceCol[MoveColor][thisRook]][MoveColor*7] = 0;
  var col = PieceCol[MoveColor][thisRook];
  if (col < 6) { col = 6; }
  while ((col > PieceCol[MoveColor][0]) || (col >= 5)) {
    if (Board[col][MoveColor*7] !== 0) { return false; }
    --col;
  }
  castleRook = thisRook;
  return true;
}

function CheckLegalityOOO() {
  if (CurrentLine.CastlingLong[MoveColor] < 0) { return false; }
  if (PieceMoveCounter[MoveColor][0] > 0) { return false; }
  // which rook is castling
  var legal = false;
  var thisRook = 0;
  while (thisRook < 16) {
    if ((PieceCol[MoveColor][thisRook] == CurrentLine.CastlingLong[MoveColor]) &&
      (PieceCol[MoveColor][thisRook] < PieceCol[MoveColor][0]) &&
      (PieceRow[MoveColor][thisRook] == MoveColor*7) &&
      (PieceType[MoveColor][thisRook] == 3)) {
      legal = true;
      break;
    }
    ++thisRook;
  }
  if (!legal) { return false; }
  if (PieceMoveCounter[MoveColor][thisRook] > 0) { return false; }

  // check no piece between king and rook
  // clear king/rook squares for Chess960
  Board[PieceCol[MoveColor][0]][MoveColor*7] = 0;
  Board[PieceCol[MoveColor][thisRook]][MoveColor*7] = 0;
  var col = PieceCol[MoveColor][thisRook];
  if (col > 2) { col = 2; }
  while ((col < PieceCol[MoveColor][0]) || (col <= 3)) {
   if (Board[col][MoveColor*7] !== 0) { return false; }
    ++col;
  }
  castleRook = thisRook;
  return true;
}

function CheckClearWay(thisPiece) {
  var stepCol = sign(mvToCol-PieceCol[MoveColor][thisPiece]);
  var stepRow = sign(mvToRow-PieceRow[MoveColor][thisPiece]);
  var startCol = PieceCol[MoveColor][thisPiece]+stepCol;
  var startRow = PieceRow[MoveColor][thisPiece]+stepRow;
  while ((startCol != mvToCol) || (startRow != mvToRow)) {
    if (Board[startCol][startRow] !== 0) { return false; }
    startCol += stepCol;
    startRow += stepRow;
  }
  return true;
}

function ClearMove(move) {
  var ss = move.length;
  var cc = -1;
  var ii = 0;
  var mm = "";
  while(ii < ss){
    cc = move.charCodeAt(ii);
    if ((cc == 45) || ((cc >= 48) && (cc <= 57)) || (cc == 61) ||
//        (cc == 35) || (cc == 43) || // patch this to pass through '+' and '#' signs
	((cc >= 65) && (cc <= 90)) || ((cc >=97) && (cc <= 122))) {
	  mm += move.charAt(ii);
    }
    ++ii;
  }
  if (mm.match('^[Oo0]-?[Oo0]-?[Oo0]$')) { return 'O-O-O'; }
  if (mm.match('^[Oo0]-?[Oo0]$')) { return 'O-O'; }
  return mm;
}

function GoToMove(thisMove) {
  var diff = thisMove - CurrentPly;
  if (diff > 0) { MoveForward(diff); }
  else { MoveBackward(-diff); }
}

function SetShortcutKeysEnabled(onOff) {
  shortcutKeysEnabled = onOff;
}

function interactivelyToggleShortcutKeys() {
  if (confirm("Shortcut keys currently " + (shortcutKeysEnabled ? "enabled" : "disabled") + ".\nToggle shortcut keys to " + (shortcutKeysEnabled ? "DISABLED" : "ENABLED") + "?")) {
    SetShortcutKeysEnabled(!shortcutKeysEnabled);
  }
}

function SetCommentsIntoMoveText(onOff) {
  commentsIntoMoveText = onOff;
}

function SetCommentsOnSeparateLines(onOff) { 
  commentsOnSeparateLines = onOff;
}

function SetAutostartAutoplay(onOff) {
  autostartAutoplay = onOff;
}

function SetAutoplayNextGame(onOff) {
  autoplayNextGame = onOff;
}

function SetShowVariations(onOff) {
  showVariations = onOff;
}

function SetInitialHalfmove(number_or_string, always) {
  if (always === true) { alwaysInitialHalfmove = true; }
  initialHalfmove = number_or_string;
  if (initialHalfmove == "start") { return; }
  if (initialHalfmove == "end") { return; }
  if (initialHalfmove == "random") { return; }
  if (initialHalfmove == "comment") { return; }
  if ((initialHalfmove = parseInt(initialHalfmove,10)) == NaN) { initialHalfmove = 0; }
}

function SetInitialGame(number_or_string) {
  if (number_or_string) { initialGame = number_or_string; }
}

// clock detection: check DGT sequence [%clk 01:02]
  
function clockFromComment(plyNum) {
  return customPgnCommentTag("clk", null, plyNum);
}

function HighlightLastMove() {
  var anchorName;

  // remove highlighting from old anchor
  if (oldAnchor != ''){
    anchorName = oldAnchor;
    theAnchor = document.getElementById(anchorName);
    if (theAnchor !== null) { theAnchor.className = 'move'; }
  }

  // find halfmove to be highlighted, negative for starting position (nothing to highlight)
  var showThisMove = CurrentPly - 1;
  if (showThisMove > StartPly + PlyNumber) { showThisMove = StartPly + PlyNumber; }

  if (theShowCommentTextObject = document.getElementById("GameLastComment")) {
    theShowCommentTextObject.innerHTML = strippedMoveComment(showThisMove);
  }
  
  // show side to move
  text = (showThisMove+1)%2 === 0 ? 'white' : 'black';
 
  if (theObject = document.getElementById("GameSideToMove"))
  { theObject.innerHTML = text; }

  // show clock if any
  if ((showThisMove+1)%2 == 1) { // white has just moved
    lastMoverClockObject = document.getElementById("GameWhiteClock");
    initialLastMoverClock = gameInitialWhiteClock[currentGame];
    beforeLastMoverClockObject = document.getElementById("GameBlackClock"); 
    initialBeforeLastMoverClock = gameInitialBlackClock[currentGame];
  } else {
    lastMoverClockObject = document.getElementById("GameBlackClock");
    initialLastMoverClock = gameInitialBlackClock[currentGame];
    beforeLastMoverClockObject = document.getElementById("GameWhiteClock"); 
    initialBeforeLastMoverClock = gameInitialWhiteClock[currentGame];
  }

  if (lastMoverClockObject !== null) {
    lastMoverClockObject.innerHTML = showThisMove+1 > StartPly ?
      clockFromComment(showThisMove+1) : initialLastMoverClock;
    // fix DGT board issue possibly missing last move clock info
    if ((lastMoverClockObject.innerHTML === "") && 
      ((LiveBroadcastDelay > 0) || (showThisMove+1 === StartPly+PlyNumber))) {
      lastMoverClockObject.innerHTML = showThisMove-1 > StartPly ?
        clockFromComment(showThisMove-1) : initialLastMoverClock;
    }
  }
  if (beforeLastMoverClockObject !== null) {
    beforeLastMoverClockObject.innerHTML = showThisMove > StartPly ?
      clockFromComment(showThisMove) : initialLastMoverClock;
  }

  // show next move
  var theShowMoveTextObject = document.getElementById("GameNextMove");
  if (theShowMoveTextObject !== null) {
    if (showThisMove + 1 >= CurrentLine.length) {
      if (CurrentLine === Moves) {
        text = gameResult[currentGame];
      } else {
        text = '--';
      }
    } else {
      text = (Math.floor((showThisMove+1)/2) + 1) + 
        ((showThisMove+1) % 2 === 0 ? '. ' : '... ') + CurrentLine[showThisMove+1].move;
    }
    theShowMoveTextObject.innerHTML = text; 
    theShowMoveTextObject.style.whiteSpace = 'nowrap';
  }

  // show last move
  theShowMoveTextObject = document.getElementById("GameLastMove");
  if (theShowMoveTextObject !== null) {
    if ((showThisMove >= StartPly) && CurrentLine[showThisMove].move) {
      text = (Math.floor(showThisMove/2) + 1) + 
       (showThisMove % 2 === 0 ? '. ' : '... ') + CurrentLine[showThisMove].move;
    } else { text = ''; }
    theShowMoveTextObject.innerHTML = text; 
    theShowMoveTextObject.style.whiteSpace = 'nowrap';
  }

//  if (showThisMove >= (StartPly-1)) {
    anchorName = 'Mv' + (showThisMove + 1) + '_' + CurrentLine.UUID;
    theAnchor = document.getElementById(anchorName);
    if (theAnchor !== null) { theAnchor.className = 'move moveOn'; }
    oldAnchor = anchorName;

    if (highlightOption) {
      if (showThisMove < StartPly) {
        highlightColFrom = highlightRowFrom = -1;
        highlightColTo   = highlightRowTo   = -1;
      } else {
        highlightColFrom = CurrentLine.HistCol[0][showThisMove] === undefined ? -1 : CurrentLine.HistCol[0][showThisMove];
        highlightRowFrom = CurrentLine.HistRow[0][showThisMove] === undefined ? -1 : CurrentLine.HistRow[0][showThisMove];
        highlightColTo   = CurrentLine.HistCol[2][showThisMove] === undefined ? -1 : CurrentLine.HistCol[2][showThisMove];
        highlightRowTo   = CurrentLine.HistRow[2][showThisMove] === undefined ? -1 : CurrentLine.HistRow[2][showThisMove];
      }
      highlightMove(highlightColFrom, highlightRowFrom, highlightColTo, highlightRowTo);
    }
//  }
}

function SetHighlightOption(on) {
  highlightOption = on;
}

function SetHighlight(on) {
  SetHighlightOption(on);
  if (on) { HighlightLastMove(); }
  else { highlightMove(-1, -1, -1, -1); }
}

var lastColFromHighlighted = -1;
var lastRowFromHighlighted = -1;
var lastColToHighlighted = -1;
var lastRowToHighlighted = -1;
function highlightMove(colFrom, rowFrom, colTo, rowTo) {
  highlightSquare(lastColFromHighlighted, lastRowFromHighlighted, false);
  highlightSquare(lastColToHighlighted, lastRowToHighlighted, false);
  if ( highlightSquare(colFrom, rowFrom, true) ) {
    lastColFromHighlighted = colFrom;
    lastRowFromHighlighted = rowFrom;
  } else { lastColFromHighlighted = lastRowFromHighlighted = -1; }
  if ( highlightSquare(colTo, rowTo, true) ) {
    lastColToHighlighted = colTo;
    lastRowToHighlighted = rowTo;
  } else { lastColToHighlighted = lastRowToHighlighted = -1; }
}

function highlightSquare(col, row, on) {
  if ((col === undefined) || (row === undefined)) { return false; }
  if (! SquareOnBoard(col, row)) { return false; }
  // locates coordinates on HTML table
  if (IsRotated) { trow = row; tcol = 7 - col; }
  else { trow = 7 - row; tcol = col; }
  if (!(theObject = document.getElementById('tcol' + tcol + 'trow' + trow))) { return false; }
  if (on) { theObject.className = (trow+tcol)%2 === 0 ? "highlightWhiteSquare" : "highlightBlackSquare"; }
  else { theObject.className = (trow+tcol)%2 === 0 ? "whiteSquare" : "blackSquare"; }
  return true;
}

// keep this aligned with the one in chrome-extension/background.html
function fixCommonPgnMistakes(text) {
  text = text.replace(/[\u00A0\u180E\u2000-\u200A\u202F\u205F\u3000]/g," "); // some "space" to plain space
  text = text.replace(/\u00BD/g,"1/2"); // "half fraction" to "1/2"
  text = text.replace(/[\u2010-\u2015]/g,"-"); // "hyphens" to "-"
  text = text.replace(/\u2024/g,"."); // "one dot leader" to "."
  text = text.replace(/[\u2025-\u2026]/g,"..."); // "two dot leader" and "ellipsis" to "..."
  return text;
}

function pgnGameFromPgnText(pgnText) {

  pgnText = fixCommonPgnMistakes(pgnText);

  // replace < and > with html entities: avoid html injection from PGN data
  pgnText = pgnText.replace(/</g, "&lt;");
  pgnText = pgnText.replace(/>/g, "&gt;");

  lines = pgnText.split("\n");
  inGameHeader = false;
  inGameBody = false;
  gameIndex = -1;
  pgnGame.length = 0;
  for(ii in lines) {

    // PGN standard: lines starting with % must be ignored
    if(lines[ii].charAt(0) == '%') { continue; }

    if(pgnHeaderTagRegExp.test(lines[ii]) === true) {
      if(!inGameHeader) {
        gameIndex++;
        pgnGame[gameIndex] = '';
      }
      inGameHeader = true;
      inGameBody = false;
    } else {
      if(inGameHeader) {
        inGameHeader = false;
        inGameBody = true;
      }
    }
    lines[ii] = lines[ii].replace(/^\s*/,"");
    lines[ii] = lines[ii].replace(/\s*$/,"");
    if (gameIndex >= 0) { pgnGame[gameIndex] += lines[ii] + ' \n'; } 
  }

  numberOfGames = pgnGame.length;

  return (gameIndex >= 0);
}

var LOAD_PGN_FROM_PGN_URL_FAIL = 0;
var LOAD_PGN_FROM_PGN_URL_OK = 1;
var LOAD_PGN_FROM_PGN_URL_UNMODIFIED = 2;
function loadPgnFromPgnUrl(pgnUrl){
  
  LiveBroadcastLastRefreshedLocal = (new Date()).toLocaleString();

  var http_request = false;
    if (window.XMLHttpRequest) { // not IE
      http_request = new XMLHttpRequest();
      if (http_request.overrideMimeType) {
        http_request.overrideMimeType('text/plain');
      }
    } else if (window.ActiveXObject) { // IE
      try { http_request = new ActiveXObject("Msxml2.XMLHTTP"); }
      catch (e) {
        try { http_request = new ActiveXObject("Microsoft.XMLHTTP"); }
        catch (e) { }
      }
    }
  if (!http_request) {
    myAlert('error: XMLHttpRequest failed for PGN URL\n' + pgnUrl, true);
    return LOAD_PGN_FROM_PGN_URL_FAIL; 
  }

  try {
    // anti-caching #1: add random parameter, only to plain URLs
    urlRandomizer = ((LiveBroadcastDelay > 0) && (pgnUrl.indexOf("?") == -1) && (pgnUrl.indexOf("#") == -1)) ? "?nocahce=" + Math.random() : "";
    http_request.open("GET", pgnUrl + urlRandomizer, false);
    // anti-caching #2: add header option
    if (LiveBroadcastDelay > 0) {
      http_request.setRequestHeader( "If-Modified-Since", LiveBroadcastLastModifiedHeader );
    }
    http_request.send(null);
  } catch(e) {
    myAlert('error: request failed for PGN URL\n' + pgnUrl, true);
    return LOAD_PGN_FROM_PGN_URL_FAIL;
  }

  if ( (http_request.readyState == 4) && 
    ((http_request.status == 200) || (http_request.status === 0) || (http_request.status == 304)) ) {

    if (http_request.status == 304) {
      if (LiveBroadcastDelay > 0) { return LOAD_PGN_FROM_PGN_URL_UNMODIFIED; }
      else { 
        myAlert('error: unexpected unmodified PGN URL when not in live mode');
        return LOAD_PGN_FROM_PGN_URL_FAIL;
      }

// dirty hack for Opera's failure reporting 304 status
    } else if (window.opera && (! http_request.responseText) && (http_request.status === 0)) {
      http_request.abort(); 
      return LOAD_PGN_FROM_PGN_URL_UNMODIFIED;
// end of dirty hack

    } else if (! pgnGameFromPgnText(http_request.responseText)) {
      myAlert('error: no games found in PGN file\n' + pgnUrl, true);
      return LOAD_PGN_FROM_PGN_URL_FAIL;
    } else {
      if (LiveBroadcastDelay > 0) {
        LiveBroadcastLastModifiedHeader = http_request.getResponseHeader("Last-Modified");
        if (LiveBroadcastLastModifiedHeader) { 
          LiveBroadcastLastModified = new Date(LiveBroadcastLastModifiedHeader); 
          LiveBroadcastLastReceivedLocal = (new Date()).toLocaleString();
        }
        else { LiveBroadcastLastModified_Reset(); }
      }
    }
  } else { 
    myAlert('error: failed reading PGN from URL\n' + pgnUrl, true);
    return LOAD_PGN_FROM_PGN_URL_FAIL;
  }

  return LOAD_PGN_FROM_PGN_URL_OK;
}

function SetPgnUrl(url) {
  pgnUrl = url;
  // hidden link for the chrome extension to detect
  if (url) { document.write("<a style='display:none;' href=" + url + "></a>"); }
}


function LiveBroadcastLastModified_Reset() {
  LiveBroadcastLastModified = new Date(0);
  LiveBroadcastLastModifiedHeader = LiveBroadcastLastModified.toUTCString();
}

function LiveBroadcastLastReceivedLocal_Reset() {
  LiveBroadcastLastReceivedLocal = 'unavailable';
}

function LiveBroadcastLastModified_ServerTime() {
  return LiveBroadcastLastModified.getTime() === 0 ? 'unavailable' : LiveBroadcastLastModifiedHeader; 
}

function pauseLiveBroadcast() {
  if (LiveBroadcastDelay === 0) { return; }
  LiveBroadcastPaused = true;
  clearTimeout(LiveBroadcastInterval);
  LiveBroadcastInterval = null;
}

function restartLiveBroadcast() {
  if (LiveBroadcastDelay === 0) { return; }
  LiveBroadcastPaused = false;
  refreshPgnSource();
}

function checkLiveBroadcastStatus() {

  if (LiveBroadcastDelay === 0) { 
    LiveBroadcastEnded = false;
    LiveBroadcastStatusString = "";
    return; 
  }

  // broadcast started yet?
  // check for fake LiveBroadcastPlaceholderPgn game when no PGN file is found
  if ((LiveBroadcastStarted === false) || ((pgnGame === undefined) ||
    ((numberOfGames == 1) && (gameEvent[0] == LiveBroadcastPlaceholderEvent)))) {
    LiveBroadcastEnded = false;
    LiveBroadcastStatusString = "live broadcast yet to start";
  } else {
    // broadcast started with good PGN
    liveGamesRunning = 0;
    for (ii=0; ii<numberOfGames; ii++) {
      if (gameResult[ii].indexOf('*') >= 0) { liveGamesRunning++; }
    }
    LiveBroadcastEnded = (liveGamesRunning === 0);

    LiveBroadcastStatusString = LiveBroadcastEnded ? "live broadcast ended" :
      "live games: " + liveGamesRunning + " &nbsp; finished: " + (numberOfGames - liveGamesRunning);
  }

  if (theObject = document.getElementById("GameLiveStatus"))
  { theObject.innerHTML = LiveBroadcastStatusString; }

  if (theObject = document.getElementById("GameLiveLastRefreshed"))
  { theObject.innerHTML = LiveBroadcastLastRefreshedLocal; }
  if (theObject = document.getElementById("GameLiveLastReceived"))
  { theObject.innerHTML = LiveBroadcastLastReceivedLocal; }
  if (theObject = document.getElementById("GameLiveLastModifiedServer"))
  { theObject.innerHTML = LiveBroadcastLastModified_ServerTime(); }
}

function restartLiveBroadcastTimeout() {
  if (LiveBroadcastDelay === 0) { return; }
  if (LiveBroadcastInterval) { clearTimeout(LiveBroadcastInterval); LiveBroadcastInterval = null; }
  checkLiveBroadcastStatus();
  needRestart = (!LiveBroadcastEnded);
  if ((needRestart === true) && (!LiveBroadcastPaused)) {
    LiveBroadcastInterval = setTimeout("refreshPgnSource()", LiveBroadcastDelay * 60000);
  }
  LiveBroadcastTicker++;
}

var LiveBroadcastFoundOldGame = false;
var LiveBroadcastGameLoadFailures = 0;
var LiveBroadcastGameLoadFailuresThreshold = 5;
var LiveBroadcastOldCurrentPly;
var LiveBroadcastOldCurrentPlyLast = false;
function refreshPgnSource() {
  if (LiveBroadcastDelay === 0) { return; }
  if (LiveBroadcastInterval) { clearTimeout(LiveBroadcastInterval); LiveBroadcastInterval = null; }
  if (LiveBroadcastDemo) {
    addedPly = 0;
    for(ii=0;ii<numberOfGames;ii++) {
      rnd = Math.random();
      if      (rnd <= 0.05) { newPly = 3; } //  5% add 3 ply
      else if (rnd <= 0.20) { newPly = 2; } // 15% add 2 ply
      else if (rnd <= 0.60) { newPly = 1; } // 40% add 1 ply
      else                  { newPly = 0; } // 40% add 0 ply
      if (gameDemoMaxPly[ii] <= gameDemoLength[ii]) { 
        gameDemoMaxPly[ii] += newPly;
        addedPly += newPly;
      }
    }    
    if (addedPly > 0) { LiveBroadcastLastReceivedLocal = (new Date()).toLocaleString(); }
  }

  loadPgnFromPgnUrlResult = loadPgnFromPgnUrl(pgnUrl);
  if (LiveBroadcastDemo && (loadPgnFromPgnUrlResult == LOAD_PGN_FROM_PGN_URL_UNMODIFIED)) {
    loadPgnFromPgnUrlResult = LOAD_PGN_FROM_PGN_URL_OK;
  }

  switch ( loadPgnFromPgnUrlResult ) {
  
    case LOAD_PGN_FROM_PGN_URL_FAIL:
      LiveBroadcastGameLoadFailures++;
      if (LiveBroadcastGameLoadFailures >= LiveBroadcastGameLoadFailuresThreshold) {
        LiveBroadcastStarted = false;
        pgnGameFromPgnText(LiveBroadcastPlaceholderPgn);
        LiveBroadcastLastModified_Reset();
        LiveBroadcastLastReceivedLocal_Reset();
        initialGame = 1;
        firstStart = true;
        textSelectOptions = '';
        LoadGameHeaders();
        Init();
        checkLiveBroadcastStatus();
        customFunctionOnPgnTextLoad();
      } else { checkLiveBroadcastStatus(); }
      break;

    case LOAD_PGN_FROM_PGN_URL_OK:
      LiveBroadcastGameLoadFailures = 0;
      LiveBroadcastStarted = true;

      oldGameWhite = gameWhite[currentGame];
      oldGameBlack = gameBlack[currentGame];
      oldGameEvent = gameEvent[currentGame];
      oldGameRound = gameRound[currentGame];
      oldGameSite  = gameSite[currentGame];
      oldGameDate  = gameDate[currentGame];

      initialGame = currentGame + 1;
      firstStart = true;
      textSelectOptions = '';

      LiveBroadcastOldCurrentPly = CurrentPly;
      LiveBroadcastOldCurrentPlyLast = (CurrentPly === StartPly + PlyNumber);

      oldAutoplay = isAutoPlayOn;
      if (isAutoPlayOn) { SetAutoPlay(false); }

      LoadGameHeaders();
      LiveBroadcastFoundOldGame = false;
      for (ii=0; ii<numberOfGames; ii++) {
        LiveBroadcastFoundOldGame = 
          (gameWhite[ii]==oldGameWhite) && (gameBlack[ii]==oldGameBlack) && 
          (gameEvent[ii]==oldGameEvent) && (gameRound[ii]==oldGameRound) &&
          (gameSite[ii] ==oldGameSite ) && (gameDate[ii] ==oldGameDate );
        if (LiveBroadcastFoundOldGame) { break; }
      }
      if (LiveBroadcastFoundOldGame) { initialGame = ii + 1; }

      if (LiveBroadcastFoundOldGame) { 
        oldInitialHalfmove = initialHalfmove; 
        if (LiveBroadcastSteppingMode) {
          initialHalfmove = LiveBroadcastOldCurrentPlyLast ? 
            LiveBroadcastOldCurrentPly+1 : LiveBroadcastOldCurrentPly;
        } else {
          initialHalfmove = LiveBroadcastOldCurrentPlyLast ? "end" : LiveBroadcastOldCurrentPly;
        }
      }
  
      Init();

      if (LiveBroadcastFoundOldGame) { 
        initialHalfmove = oldInitialHalfmove; 
      } 
  
      checkLiveBroadcastStatus();
      customFunctionOnPgnTextLoad();

      if (LiveBroadcastFoundOldGame) {
        if (LiveBroadcastSteppingMode) {
          if (oldAutoplay || LiveBroadcastOldCurrentPlyLast) { SetAutoPlay(true); }
        } else {
          if (oldAutoplay) { SetAutoPlay(true); }
        }
      }

      break;

    case LOAD_PGN_FROM_PGN_URL_UNMODIFIED: 
      LiveBroadcastGameLoadFailures = 0;
      checkLiveBroadcastStatus();
      break;

    default:
      break;

  }

  restartLiveBroadcastTimeout();
}


function createBoard(){

  if (theObject = document.getElementById("GameBoard")) {
    theObject.innerHTML = '<DIV STYLE="font-size: small; font-family: sans-serif; ' +
      'padding: 10px; text-align: center;">' + 
      '...loading PGN data<br />please wait...</DIV>';
  }

  if (pgnUrl) {
    switch (loadPgnFromPgnUrl(pgnUrl)) {
      case LOAD_PGN_FROM_PGN_URL_OK:
        if (LiveBroadcastDelay > 0) { LiveBroadcastStarted = true; }
        Init();
        if (LiveBroadcastDelay > 0) { checkLiveBroadcastStatus(); }
        customFunctionOnPgnTextLoad();
        return;

      case LOAD_PGN_FROM_PGN_URL_FAIL:
        if (LiveBroadcastDelay === 0) {
          pgnGameFromPgnText(alertPgnHeader);
          Init();
          customFunctionOnPgnTextLoad();
          myAlert('error: failed loading games from PGN URL\n' + pgnUrl, true);
        } else { // live broadcast: wait for live show start
          LiveBroadcastStarted = false;
          LiveBroadcastLastModified_Reset();
          LiveBroadcastLastReceivedLocal_Reset();
          pgnGameFromPgnText(LiveBroadcastPlaceholderPgn); 
          Init();
	  checkLiveBroadcastStatus();
          customFunctionOnPgnTextLoad();
        }
        return;

      case LOAD_PGN_FROM_PGN_URL_UNMODIFIED:
        if (LiveBroadcastDelay > 0) { checkLiveBroadcastStatus(); }
        return;
     
      default:
        return;
 
    }
  } else if ( document.getElementById("pgnText") ) {
    if (document.getElementById("pgnText").tagName.toLowerCase() == "textarea") {
      tmpText = document.getElementById("pgnText").value;
    } else { // compatibility with pgn4web up to 1.77: <span> used for pgnText
      tmpText = document.getElementById("pgnText").innerHTML;
      // fixes browser issue removing \n from innerHTML
      if (tmpText.indexOf('\n') < 0) { tmpText = tmpText.replace(/((\[[^\[\]]*\]\s*)+)/g, "\n$1\n"); }
      // fixes browser issue replacing quotes with &quot; e.g. blackberry
      if (tmpText.indexOf('"') < 0) { tmpText = tmpText.replace(/(&quot;)/g, '"'); }
    }

    // no html header => add emptyPgnHeader
    if (pgnHeaderTagRegExp.test(tmpText) === false) { tmpText = emptyPgnHeader + tmpText; }

    if ( pgnGameFromPgnText(tmpText) ) {
      Init(); 
      customFunctionOnPgnTextLoad();
    } else {
      pgnGameFromPgnText(alertPgnHeader);
      Init();
      customFunctionOnPgnTextLoad();
      myAlert('error: no games found in PGN text', true);
    }   
    return;
  } else {
    pgnGameFromPgnText(alertPgnHeader);
    Init();
    customFunctionOnPgnTextLoad();
    myAlert('error: missing PGN URL location or pgnText in the HTML file', true);
    return;
  }
}

function setCurrentGameFromInitialGame() {
  switch (initialGame) {
    case "first":
      currentGame = 0;
      break;
    case "last":
      currentGame = numberOfGames - 1;
      break;
    case "random":
      currentGame = Math.floor(Math.random()*numberOfGames);
      break;
    default:
      if (isNaN(parseInt(initialGame,10))) { 
        currentGame = gameNumberSearchPgn(initialGame, false, true);
        if (!currentGame) { currentGame = 0; }
      } else {
        initialGame = parseInt(initialGame,10);
        initialGame = initialGame < 0 ? -Math.floor(-initialGame) : Math.floor(initialGame);
        if (initialGame < -numberOfGames) { currentGame = 0; }
        else if (initialGame < 0) { currentGame = numberOfGames + initialGame; }
        else if (initialGame === 0) { currentGame = Math.floor(Math.random()*numberOfGames); }
        else if (initialGame <= numberOfGames) { currentGame = (initialGame - 1); } 
        else { currentGame = numberOfGames - 1; }
      }
      break;
  }
}

function GoToInitialHalfmove() {
  switch (initialHalfmove) {
    case "start":
      GoToMove(0);
      break;
    case "end":
      GoToMove(StartPly + PlyNumber);
      break;
    case "random":
      GoToMove(StartPly + Math.floor(Math.random()*(StartPly + PlyNumber)));
      break;
    case "comment":
      GoToMove(0);
      MoveToNextComment();
      break;
    default:
      if (isNaN(initialHalfmove)) { initialHalfmove = 0; }
      initialHalfmove = parseInt(initialHalfmove,10);
      initialHalfmove = initialHalfmove < 0 ? -Math.floor(-initialHalfmove) : Math.floor(initialHalfmove);
      if (initialHalfmove < -3) { initialHalfmove = 0; }
      if (initialHalfmove == -3) { GoToMove(StartPly + PlyNumber); }
      else if (initialHalfmove == -2) { GoToMove(0); MoveToNextComment(); }
      else if (initialHalfmove == -1) { GoToMove(StartPly + Math.floor(Math.random()*(StartPly + PlyNumber))); }
      else { GoToMove(Math.floor(initialHalfmove)); }
      break;
  }
}

function Init(nextGame){

  if (nextGame !== undefined) {
    if ((! isNaN(nextGame)) && (nextGame >= 0) && (nextGame < numberOfGames)) {
      currentGame = parseInt(nextGame,10);
    } else { return; }
  }

  if (isAutoPlayOn) { SetAutoPlay(false); }

  InitImages();
  if (firstStart) {
    LoadGameHeaders();
    setCurrentGameFromInitialGame();
  }

  LineLookup = new Array();
  Moves = new MoveArray();
  CurrentLine = Moves;

  if ((gameSetUp[currentGame] !== undefined) && (gameSetUp[currentGame] != "1")) { InitFEN(); }
  else { InitFEN(gameFEN[currentGame]); }
  
  OpenGame(currentGame);
  
  RefreshBoard();
  CurrentPly = StartPly;
  HighlightLastMove(); 
  if (firstStart || alwaysInitialHalfmove) { GoToInitialHalfmove(); }
  else { customFunctionOnMove(); }
  // customFunctionOnMove here for consistency: null move starting new game

  if ((firstStart) && (autostartAutoplay)) { SetAutoPlay(true); }

  customFunctionOnPgnGameLoad();

  firstStart = false;
}


function InitFEN(startingFEN) {
  FenString = startingFEN !== undefined ? startingFEN : FenStringStart;

  // board reset
  var ii, jj;
  for (ii = 0; ii < 8; ++ii) {
    for (jj = 0; jj < 8; ++jj) {
      Board[ii][jj] = 0;
    }
  }

  // initial position
  var color, pawn;
  StartPly = 0;
  MoveCount = StartPly;
  MoveColor = StartPly % 2;
  StartMove = 0;

  var newEnPassant = false;
  var newEnPassantCol;
  CurrentLine.CastlingLong[0] = CurrentLine.CastlingLong[1] = 0;
  CurrentLine.CastlingShort[0] = CurrentLine.CastlingShort[1] = 7;
  var InitialHalfMoveClock = 0;

  if (FenString == FenStringStart) {
    for (color = 0; color < 2; ++color) {
      PieceType[color][0] = 1; // King
      PieceCol[color][0]  = 4;
      PieceType[color][1] = 2; // Queen
      PieceCol[color][1]  = 3;
      PieceType[color][6] = 3; // Rooks
      PieceType[color][7] = 3;
      PieceCol[color][6]  = 0;
      PieceCol[color][7]  = 7;
      PieceType[color][4] = 4; // Bishops
      PieceType[color][5] = 4;
      PieceCol[color][4]  = 2;
      PieceCol[color][5]  = 5;
      PieceType[color][2] = 5; // Knights
      PieceType[color][3] = 5;
      PieceCol[color][2]  = 1;
      PieceCol[color][3]  = 6;
      for (pawn = 0; pawn < 8; ++pawn) {
	PieceType[color][pawn+8] = 6;
	PieceCol[color][pawn+8]  = pawn;
      }
      for (ii = 0; ii < 16; ++ii) {
	PieceMoveCounter[color][ii] = 0;
	PieceRow[color][ii] = (1-color) * Math.floor(ii/8) + color * (7-Math.floor(ii/8));
      }
      for (ii = 0; ii < 16; ii++) {
        var col = PieceCol[color][ii];
        var row = PieceRow[color][ii];
        Board[col][row] = (1-2*color)*PieceType[color][ii];
      }
    }
  } else {
    var cc, kk, ll, nn, mm;
    for (ii = 0; ii < 2; ii++) {
      for (jj = 0; jj < 16; jj++) {
        PieceType[ii][jj] = -1;
        PieceCol[ii][jj] = 0;
        PieceRow[ii][jj] = 0;
        PieceMoveCounter[ii][jj] = 0;
      }
    }

    ii = 0; jj = 7; ll = 0; nn = 1; mm = 1; cc = FenString.charAt(ll++);
    while (cc != " ") {
      if (cc == "/") {
        if (ii != 8) {
          myAlert("error: invalid FEN ("+ll+") in game "+(currentGame+1)+"\n"+FenString, true);
          InitFEN();
          return;
        }
        ii = 0;
        jj--;
      }
      if (ii == 8) {
        myAlert("error: invalid FEN ("+ll+") in game "+(currentGame+1)+"\n"+FenString, true);
        InitFEN();
        return;
      }
      if (!isNaN(cc)) {
        ii += parseInt(cc,10);
        if ((ii < 0) || (ii > 8)) {
          myAlert("error: invalid FEN ("+ll+") in game "+(currentGame+1)+"\n"+FenString, true);
          InitFEN();
          return;
        }
      }
      if (cc.charCodeAt(0) == FenPieceName.toUpperCase().charCodeAt(0)) {
        if (PieceType[0][0] != -1) {
          myAlert("error: invalid FEN ("+ll+") in game "+(currentGame+1)+"\n"+FenString, true);
          InitFEN();
          return;
        }     
        PieceType[0][0] = 1;
        PieceCol[0][0] = ii;
        PieceRow[0][0] = jj;
        ii++;
      }
      if (cc.charCodeAt(0) == FenPieceName.toLowerCase().charCodeAt(0)) {
        if (PieceType[1][0] != -1) {
          myAlert("error: invalid FEN ("+ll+") in game "+(currentGame+1)+"\n"+FenString, true);
          InitFEN();
          return;
        }  
        PieceType[1][0] = 1;
        PieceCol[1][0] = ii;
        PieceRow[1][0] = jj;
        ii++;
      }
      for (kk = 1; kk < 6; kk++) {
        if (cc.charCodeAt(0) == FenPieceName.toUpperCase().charCodeAt(kk)) {
          if (nn == 16) {
            myAlert("error: invalid FEN ("+ll+") in game "+(currentGame+1)+"\n"+FenString, true);
            InitFEN();
            return;
          }          
          PieceType[0][nn] = kk+1;
          PieceCol[0][nn] = ii;
          PieceRow[0][nn] = jj;
          nn++;
          ii++;
        }
        if (cc.charCodeAt(0) == FenPieceName.toLowerCase().charCodeAt(kk)) {
          if (mm==16) {
            myAlert("error: invalid FEN ("+ll+") in game "+(currentGame+1)+"\n"+FenString, true);
            InitFEN();
            return;
          }  
          PieceType[1][mm] = kk+1;
          PieceCol[1][mm] = ii;
          PieceRow[1][mm] = jj;
          mm++;
          ii++;
        }
      }
      cc = ll < FenString.length ? FenString.charAt(ll++) : " ";
    }
    if ((ii != 8) || (jj !== 0)) {
      myAlert("error: invalid FEN ("+ll+") in game "+(currentGame+1)+"\n"+FenString, true);
      InitFEN();
      return;
    }
    if ((PieceType[0][0] == -1) || (PieceType[1][0] == -1)) {
      myAlert("error: invalid FEN missing King in game "+(currentGame+1)+"\n"+FenString, true);
      InitFEN();
      return;
    }
    if (ll == FenString.length) {
      FenString += " w ";
      FenString += FenPieceName.toUpperCase().charAt(0);
      FenString += FenPieceName.toUpperCase().charAt(1);
      FenString += FenPieceName.toLowerCase().charAt(0);
      FenString += FenPieceName.toLowerCase().charAt(1);      
      FenString += " - 0 1";
      ll++;
    }
    cc = FenString.charAt(ll++);
    if ((cc == "w") || (cc == "b")) {
      if (cc == "b") { 
        StartMove=1;
        StartPly += 1;
        MoveColor = 1;
      }
    } else {
      myAlert("error: invalid FEN ("+ll+") invalid active color in game "+(currentGame+1)+"\n"+FenString, true);
      return;
    }

    // set board
    for (color = 0; color < 2; ++color) {
      for (ii = 0; ii < 16; ii++) {
        if (PieceType[color][ii] != -1) {
   	  col = PieceCol[color][ii];
	  row = PieceRow[color][ii];
	  Board[col][row] = (1-2*color)*(PieceType[color][ii]);
	}
      }
    }
          
    ll++;
    if (ll >= FenString.length) {
      myAlert("error: invalid FEN ("+ll+") missing castling availability in game "+(currentGame+1)+"\n"+FenString, true);
      return;
    }
    CurrentLine.CastlingShort[0] = CurrentLine.CastlingLong[0] = CurrentLine.CastlingShort[1] = CurrentLine.CastlingLong[1] = -1;
    cc = FenString.charAt(ll++);
    while (cc!=" ") {
      if (cc.charCodeAt(0) == FenPieceName.toUpperCase().charCodeAt(0)) {
        for (CurrentLine.CastlingShort[0] = 7; CurrentLine.CastlingShort[0] >= 0; CurrentLine.CastlingShort[0]--) {
          if (Board[CurrentLine.CastlingShort[0]][0] == 3) { break; }
        }
        if (CurrentLine.CastlingShort[0] < 0) {
          myAlert("error: invalid FEN ("+ll+") missing Rook at castling column " + cc, true);
          CurrentLine.CastlingShort[0] = -1;
        }
      }
      if (cc.charCodeAt(0) == FenPieceName.toUpperCase().charCodeAt(1)) {
        for (CurrentLine.CastlingLong[0] = 0; CurrentLine.CastlingLong[0] <= 7; CurrentLine.CastlingLong[0]++) {
          if (Board[CurrentLine.CastlingLong[0]][0] == 3) { break; }
        }
        if (CurrentLine.CastlingLong[0] > 7) {
          myAlert("error: invalid FEN ("+ll+") missing Rook at castling column " + cc, true);
          CurrentLine.CastlingLong[0] = -1;
        }
      }
      if (cc.charCodeAt(0) == FenPieceName.toLowerCase().charCodeAt(0)) {
        for (CurrentLine.CastlingShort[1] = 7; CurrentLine.CastlingShort[1] >= 0; CurrentLine.CastlingShort[1]--) {
          if (Board[CurrentLine.CastlingShort[1]][7] == -3) { break; }
        }
        if (CurrentLine.CastlingShort[1] < 0) {
          myAlert("error: invalid FEN ("+ll+") missing Rook at castling column " + cc, true);
          CurrentLine.CastlingShort[1] = -1;
        }
      }
      if (cc.charCodeAt(0) == FenPieceName.toLowerCase().charCodeAt(1)) {
        for (CurrentLine.CastlingLong[1] = 0; CurrentLine.CastlingLong[1] <= 7; CurrentLine.CastlingLong[1]++) {
          if (Board[CurrentLine.CastlingLong[1]][7] == -3) { break; }
        }
        if (CurrentLine.CastlingLong[1] > 7) {
          myAlert("error: invalid FEN ("+ll+") missing Rook at castling column " + cc, true);
          CurrentLine.CastlingLong[1] = -1;
        }
      }
      castlingRookCol = columnsLetters.toUpperCase().indexOf(cc);
      if (castlingRookCol >= 0) { color = 0; }
      else { 
        castlingRookCol = columnsLetters.toLowerCase().indexOf(cc);
        if (castlingRookCol >= 0) { color = 1; }
      }
      if (castlingRookCol >= 0) {
        if (Board[castlingRookCol][color*7] == (1-2*color) * 3) {
          if (castlingRookCol > PieceCol[color][0]) { CurrentLine.CastlingShort[color] = castlingRookCol; }
          if (castlingRookCol < PieceCol[color][0]) { CurrentLine.CastlingLong[color] = castlingRookCol; }
        } else {
          myAlert("error: invalid FEN ("+ll+") missing Rook at castling column " + cc, true);
        }
      }
      cc = ll<FenString.length ? FenString.charAt(ll++) : " ";
    }

    if (ll >= FenString.length) {
      myAlert("error: invalid FEN ("+ll+") missing en passant target square in game "+(currentGame+1)+"\n"+FenString, true);
      return;
    }
    cc = FenString.charAt(ll++);
    while (cc != " ") {
      if ((cc.charCodeAt(0)-97 >= 0) && (cc.charCodeAt(0)-97 <= 7)) {
        newEnPassant = true;
        newEnPassantCol = cc.charCodeAt(0)-97; 
      }
      cc = ll<FenString.length ? FenString.charAt(ll++) : " ";
    }
    if (ll >= FenString.length) {
//      myAlert("error: invalid FEN ("+ll+") missing halfmove clock in game "+(currentGame+1)+"\n"+FenString, true);
        CurrentLine.InitialHalfMoveClock = 0;
        StartPly = 0;
        CurrentLine.HistEnPassant[StartPly-1] = newEnPassant;
        CurrentLine.HistEnPassantCol[StartPly-1] = newEnPassantCol;
        return;
    }
    CurrentLine.InitialHalfMoveClock = 0;
    cc = FenString.charAt(ll++);
    while (cc != " ") {
      if (isNaN(cc)) {
        myAlert("error: invalid FEN ("+ll+") invalid halfmove clock in game "+(currentGame+1)+"\n"+FenString+" (" + cc +")", true);
        return;
      }
      CurrentLine.InitialHalfMoveClock = CurrentLine.InitialHalfMoveClock*10+parseInt(cc,10);
      cc = ll<FenString.length ? FenString.charAt(ll++) : " ";
    }
    if (ll >= FenString.length) {
//      myAlert("error: invalid FEN ("+ll+") missing fullmove number in game "+(currentGame+1)+"\n"+FenString, true);
      StartPly = 0;
      CurrentLine.HistEnPassant[StartPly-1] = newEnPassant;
      CurrentLine.HistEnPassantCol[StartPly-1] = newEnPassantCol;
      return;
    }
    cc = FenString.substring(ll++);
    if (isNaN(cc)) {
      myAlert("error: invalid FEN ("+ll+") invalid fullmove number in game "+(currentGame+1)+"\n"+FenString, true);
      return;
    }
    if (cc <= 0) {
      myAlert("error: invalid FEN ("+ll+") invalid fullmove number in game "+(currentGame+1)+"\n"+FenString, true);
      return;
    }
    StartPly += 2*(parseInt(cc,10)-1);

    CurrentLine.HistEnPassant[StartPly-1] = newEnPassant;
    CurrentLine.HistEnPassantCol[StartPly-1] = newEnPassantCol;
  }
}

function SetImageType(extension) {
  imageType = extension;
}

function InitImages() {
  if (ImagePathOld === ImagePath) { return; }

  if ((ImagePath.length > 0) && (ImagePath[ImagePath.length-1] != '/')) {
    ImagePath += '/';
  }

  ClearImg = new Image();
  ClearImg.src = ImagePath+'clear.'+imageType;

  var color;
  ColorName = new Array ("w", "b");
  for (color = 0; color < 2; ++color) {
    PiecePicture[color][1] = new Image();
    PiecePicture[color][1].src = ImagePath + ColorName[color] + 'k.'+imageType;
    PiecePicture[color][2] = new Image();
    PiecePicture[color][2].src = ImagePath + ColorName[color] + 'q.'+imageType;
    PiecePicture[color][3] = new Image();
    PiecePicture[color][3].src = ImagePath + ColorName[color] + 'r.'+imageType;
    PiecePicture[color][4] = new Image();
    PiecePicture[color][4].src = ImagePath + ColorName[color] + 'b.'+imageType;
    PiecePicture[color][5] = new Image();
    PiecePicture[color][5].src = ImagePath + ColorName[color] + 'n.'+imageType;
    PiecePicture[color][6] = new Image();
    PiecePicture[color][6].src = ImagePath + ColorName[color] + 'p.'+imageType;
  }
  ImagePathOld = ImagePath;
}


function IsCheck(col, row, color) {
  var ii, jj;
  var sign = 2*color-1; // white or black

  // other king giving check?
  if ((Math.abs(PieceCol[1-color][0]-col) <= 1) &&
      (Math.abs(PieceRow[1-color][0]-row) <= 1)) { return true; }

  // knight giving check?
  for (ii = -2; ii <= 2; ii += 4) {
    for(jj = -1; jj <= 1; jj += 2) {
      if (SquareOnBoard(col+ii, row+jj)) {
	if (Board[col+ii][row+jj] == sign*5) { return true; }
      }
      if (SquareOnBoard(col+jj, row+ii)) {
	if (Board[col+jj][row+ii] == sign*5) { return true; }
      }
    }
  }

  // pawn giving check?
  for (ii = -1; ii <= 1; ii += 2) {
    if (SquareOnBoard(col+ii, row-sign)) {
      if (Board[col+ii][row-sign] == sign*6) { return true; }
    }
  }

  // queens, rooks and bishops?
  for (ii = -1; ii <= 1; ++ii) {
    for (jj = -1; jj <= 1; ++jj) {
      if ((ii !== 0) || (jj !== 0)) {
	var checkCol  = col+ii;
	var checkRow  = row+jj;
	var thisPiece = 0;

	while (SquareOnBoard(checkCol, checkRow) && (thisPiece === 0)) {
	  thisPiece = Board[checkCol][checkRow];
	  if (thisPiece === 0){
	    checkCol += ii;
	    checkRow += jj;
	  } else {
	    if (thisPiece  == sign*2) { return true; }
	    if ((thisPiece == sign*3) && ((ii === 0) || (jj === 0))) { return true; }
	    if ((thisPiece == sign*4) && ((ii !== 0) && (jj !== 0))) { return true; }
	  }
	}
      }
    }
  }
  return false;
}


function fixRegExp(exp) {
  return exp.replace(/([\[\]\(\)\{\}\.\*\+\^\$\|\?\\])/g, "\\$1");
}

function LoadGameHeaders(){
  var ii;
  var parse;

  gameEvent.length = gameSite.length = gameRound.length = gameDate.length = 0;
  gameWhite.length = gameBlack.length = gameResult.length = 0;
  gameSetUp.length = gameFEN.length = 0;
  gameInitialWhiteClock.length = gameInitialBlackClock.length = 0;
  gameVariant.length = 0;

  pgnHeaderTagRegExpGlobal.lastIndex = 0; // resets global regular expression
  for (ii = 0; ii < numberOfGames; ++ii) {
    var ss = pgnGame[ii];
    gameEvent[ii] = gameSite[ii] = gameRound[ii] = gameDate[ii] = "";
    gameWhite[ii] = gameBlack[ii] = gameResult[ii] = "";
    gameInitialWhiteClock[ii] = gameInitialBlackClock[ii] = "";
    gameVariant[ii] = "";
    while ((parse = pgnHeaderTagRegExpGlobal.exec(ss)) !== null) {
      if      (parse[1] == 'Event')      { gameEvent[ii]  = parse[2]; }
      else if (parse[1] == 'Site')       { gameSite[ii]   = parse[2]; }
      else if (parse[1] == 'Round')      { gameRound[ii]  = parse[2]; }
      else if (parse[1] == 'Date')       { gameDate[ii]   = parse[2]; }
      else if (parse[1] == 'White')      { gameWhite[ii]  = parse[2]; }
      else if (parse[1] == 'Black')      { gameBlack[ii]  = parse[2]; }
      else if (parse[1] == 'Result')     { gameResult[ii] = parse[2]; }
      else if (parse[1] == 'SetUp')      { gameSetUp[ii]  = parse[2]; }
      else if (parse[1] == 'FEN')        { gameFEN[ii]    = parse[2]; }
      else if (parse[1] == 'WhiteClock') { gameInitialWhiteClock[ii] = parse[2]; }
      else if (parse[1] == 'BlackClock') { gameInitialBlackClock[ii] = parse[2]; }
      else if (parse[1] == 'Variant')    { gameVariant[ii] = parse[2]; }
    }
  }
  // TODO: do we really need to do this? it's ugly, forces reparsing of the pgn data extra times, etc.
  if ((LiveBroadcastDemo) && (numberOfGames > 0)) {
    for (ii = 0; ii < numberOfGames; ++ii) {
       if (gameDemoLength[ii] === undefined) {
         LineLookup = new Array();
         Moves = new MoveArray();
         CurrentLine = Moves;
         InitFEN(gameFEN[ii]);
         ParsePGNGameString(pgnGame[ii], false);
         gameDemoLength[ii] = PlyNumber;
       }
       if (gameDemoMaxPly[ii] === undefined) { gameDemoMaxPly[ii] = 0; }
       if (gameDemoMaxPly[ii] <= gameDemoLength[ii]) { gameResult[ii] = '*'; }
    }
  }
  return;
}


function MoveBackward(diff, refresh) {
  if (refresh == null) { refresh = true; }

  // CurrentPly counts from 1, starting position 0
  var goFromPly  = CurrentPly - 1;
  var goToPly    = goFromPly  - diff;
  if (goToPly < StartPly) { goToPly = StartPly-1; }

  // reconstruct old position ply by ply
  for(var thisPly = goFromPly; thisPly > goToPly; --thisPly) {
    CurrentPly--;
    MoveColor = 1-MoveColor;

    // moved piece back to original square
    var chgPiece = CurrentLine.HistPieceId[0][thisPly];
    if (chgPiece == undefined) {
      continue;//chgPiece = undefined;
    }
    Board[PieceCol[MoveColor][chgPiece]][PieceRow[MoveColor][chgPiece]] = 0;

    Board[CurrentLine.HistCol[0][thisPly]][CurrentLine.HistRow[0][thisPly]] = CurrentLine.HistType[0][thisPly] * (1-2*MoveColor);
    PieceType[MoveColor][chgPiece] = CurrentLine.HistType[0][thisPly];
    PieceCol[MoveColor][chgPiece] = CurrentLine.HistCol[0][thisPly];
    PieceRow[MoveColor][chgPiece] = CurrentLine.HistRow[0][thisPly];
    PieceMoveCounter[MoveColor][chgPiece]--;

    // castling: rook back to original square
    chgPiece = CurrentLine.HistPieceId[1][thisPly];
    if ((chgPiece >= 0) && (chgPiece < 16)) {
      Board[PieceCol[MoveColor][chgPiece]][PieceRow[MoveColor][chgPiece]] = 0;
      Board[CurrentLine.HistCol[1][thisPly]][CurrentLine.HistRow[1][thisPly]] = CurrentLine.HistType[1][thisPly] * (1-2*MoveColor);
      PieceType[MoveColor][chgPiece] = CurrentLine.HistType[1][thisPly];
      PieceCol[MoveColor][chgPiece] = CurrentLine.HistCol[1][thisPly];
      PieceRow[MoveColor][chgPiece] = CurrentLine.HistRow[1][thisPly];
      PieceMoveCounter[MoveColor][chgPiece]--;
    } 

    // capture: captured piece back to original square
    chgPiece -= 16;
    if ((chgPiece >= 0) && (chgPiece < 16)) {
      Board[PieceCol[1-MoveColor][chgPiece]][PieceRow[1-MoveColor][chgPiece]] = 0;
      Board[CurrentLine.HistCol[1][thisPly]][CurrentLine.HistRow[1][thisPly]] = CurrentLine.HistType[1][thisPly] * (2*MoveColor-1);
      PieceType[1-MoveColor][chgPiece] = CurrentLine.HistType[1][thisPly];
      PieceCol[1-MoveColor][chgPiece] = CurrentLine.HistCol[1][thisPly];
      PieceRow[1-MoveColor][chgPiece] = CurrentLine.HistRow[1][thisPly];
      PieceMoveCounter[1-MoveColor][chgPiece]--;
    } 
  }
  if (!refresh) return;
  // old position reconstructed: refresh board
  if (CurrentLine != Moves && CurrentLine[goToPly].move == '') {
    GoToMoveInLine(goToPly+1, CurrentLine.parentVar.UUID);
    return;
  }
  RefreshBoard();
  HighlightLastMove(); 

  // autoplay: restart timeout
  if (AutoPlayInterval) { clearTimeout(AutoPlayInterval); AutoPlayInterval = null; }
  if (isAutoPlayOn) {
    if(goToPly >= StartPly) { AutoPlayInterval=setTimeout("MoveBackward(1)", Delay); }
    else { SetAutoPlay(false); }
  } 
  customFunctionOnMove();
}


function MoveForward(diff, refresh) {
  if (refresh == null) { refresh = true; }
  // CurrentPly counts from 1, starting position 0
  goToPly = CurrentPly + parseInt(diff,10);

  if (goToPly > CurrentLine.length) { goToPly = CurrentLine.length; }

  // reach to selected move checking legality
  var parse = false;
  for(var thisPly = CurrentPly; thisPly < goToPly; ++thisPly) {
    if (CurrentLine[thisPly].isempty()) break;
    var move = CurrentLine[thisPly].move;
    if (! (parse = ParseMove(move, thisPly))) {
      text = (Math.floor(thisPly / 2) + 1) + ((thisPly % 2) === 0 ? '. ' : '... ');
      myAlert('error: invalid ply ' + text + move + ' in game ' + (currentGame+1), true);
      break;
    }
    MoveColor = 1-MoveColor; 
  }

  // new position: refresh board and update ply count
  CurrentPly = thisPly;
  if (!refresh) return;

  RefreshBoard();
  HighlightLastMove(); 

  // autoplay: restart timeout
  if (AutoPlayInterval) { clearTimeout(AutoPlayInterval); AutoPlayInterval = null; }
  if (!parse) { SetAutoPlay(false); } 
  else if (thisPly == goToPly) {
    if (isAutoPlayOn) {
      if (goToPly < CurrentLine.length) {
        AutoPlayInterval=setTimeout("MoveForward(1)", Delay);
      } else {
        if (autoplayNextGame) { AutoPlayInterval=setTimeout("AutoplayNextGame()", Delay); }
        else { SetAutoPlay(false); }
      }
    }
  }
  customFunctionOnMove();
}

function AutoplayNextGame() {
  if (fatalErrorNumSinceReset === 0) {
    if (numberOfGames > 0) {
      Init((currentGame + 1) % numberOfGames);
      if ((numberOfGames > 1) || (PlyNumber > 0)) {
        SetAutoPlay(true);
        return;
      }
    }
  }
  SetAutoPlay(false);
}

function MoveToNextComment() {
  for(ii=CurrentPly+1; ii<=StartPly+PlyNumber; ii++) {
    if (strippedMoveComment(ii-1)) { GoToMove(ii); break; }
  }
}

function MoveToPrevComment() {
  for(ii=(CurrentPly-1); ii>=0; ii--) {
    if (strippedMoveComment(ii-1)) { GoToMove(ii); break; }
  }
}


function OpenGame(gameId) {
  ParsePGNGameString(pgnGame[gameId], true);
  InsertCheckAndMate();
  currentGame = gameId;
 
  if (LiveBroadcastDemo) {
    if (gameDemoMaxPly[gameId] <= PlyNumber) { PlyNumber = gameDemoMaxPly[gameId]; }
  }
 
  PrintHTML();
}

function IsMate(thisPly) {
  var move;

  for (var i=0; i<8; ++i) {
    for (var j=0; j<8; ++j) {
      if ((MoveColor && Board[j][i] < 0) || (!MoveColor && Board[j][i] > 0)){
        for (var x=0; x<8; ++x) {
          for (var y=0; y<8; ++y) {
            move = FenPieceName.toUpperCase().charAt(Math.abs(Board[j][i])-1);
            if (move == "P") move = "";
            move += String.fromCharCode('a'.charCodeAt(0) + j)
            move += (i+1).toString();
            if (Board[y][x]){
              move += 'x';
            }
            move += String.fromCharCode('a'.charCodeAt(0) + y)
            move += (x+1).toString();
            if (ParseMove(move, thisPly+1)) {
              UndoMove(thisPly+1);
              return false;
            }
          }
        }
      }
    }
  }
  return true;
}

function InsertCheckAndMate() {
  CurrentPly = StartPly;
  Moves.FEN = CurrentFEN(); // insert a FEN for every variation array (incl main line)
  CurrentLine = Moves;
  DoInsertCheckAndMate(StartPly);
}

function DoInsertCheckAndMate(Ply) {
  var thisPly;
  for (thisPly = Ply; thisPly < CurrentLine.length; ++thisPly) {
    if (CurrentLine[thisPly].isempty()) continue; // should just break, but whatever

    // insert for variations first
    var ct = CurrentLine[thisPly].varcount();
    for (var i = 0; i < ct; i++) {
      CurrentLine[thisPly].getvar(i).FEN = CurrentFEN(CurrentLine[thisPly].getvar(i));
      CurrentLine = CurrentLine[thisPly].getvar(i);
      DoInsertCheckAndMate(thisPly);
      CurrentLine = CurrentLine.parentVar;
//    CurrentPly = thisPly;
    }

    MoveForward(1, false);
    if (IsCheck(PieceCol[MoveColor][0], PieceRow[MoveColor][0], MoveColor)) {
      if (IsMate(thisPly)) {
        CurrentLine[thisPly].move += '#';
      } else{
        CurrentLine[thisPly].move += '+';
      }
    }
  }
//  InitFEN(CurrentLine.FEN);
  MoveBackward(thisPly-Ply, false); 
}

function ParsePGNGameString(gameString, recurse) {

  var ss = gameString;
  // remove PGN tags and spaces at the end 
  ss = ss.replace(pgnHeaderTagRegExpGlobal, ''); 
  ss = ss.replace(/^\s/, '');
  ss = ss.replace(/\s$/, '');
  PlyNumber = 0;
//  for (ii=0; ii<StartPly; ii++) { CurrentLine[ii] = new MoveObject(ii); } // not necessary
  // CurrentLine[StartPly+PlyNumber].comment=''; // jb
  CurrentLine = Moves;
  DoParsePGNGameString(PlyNumber-1, ss, 0, recurse);
  // only for main line
  PlyNumber = Moves.length;
}

function getClassName(obj) {
  if (typeof obj != "object" || obj === null) return false;
  return /(\w+)\(/.exec(obj.constructor.toString())[1];
}

function DoParsePGNGameString(Ply, ss, start, recurse) {
  var push = 0;
  var localPly = Ply;
  
  // make a single object at the currentPly-1 for pre-comments
  if (!CurrentLine[StartPly+localPly]) CurrentLine[StartPly+localPly] = new MoveObject(StartPly+localPly); // jb
  
  for ( ; start<ss.length; start++) {
  
    var curslice = (ss.slice(start));
    switch (ss.charAt(start)) {
      case ' ':
      case '\b':
      case '\f':
      case '\n':
      case '\r':
      case '\t':
        break;

      case '$':
        commentStart = start;
        commentEnd = commentStart + 1;
        while ('0123456789'.indexOf(ss.charAt(commentEnd)) >= 0) {
          commentEnd++;
          if (commentEnd == ss.length) { break; }
        }
        if (CurrentLine[StartPly+localPly].comment.length>0) { CurrentLine[StartPly+localPly].comment += ' '; }
        CurrentLine[StartPly+localPly].comment += ss.substring(commentStart, commentEnd);
        start = commentEnd-1;
        break;
      
      case '!':
      case '?':
        commentStart = start;
        commentEnd = commentStart + (((ss.charAt(start+1) == '?') || (ss.charAt(start+1) == '!')) ? 2 : 1);
        if (CurrentLine[StartPly+localPly].comment.length>0) { CurrentLine[StartPly+localPly].comment += ' '; }
        CurrentLine[StartPly+localPly].comment += ss.substring(commentStart, commentEnd);
        start = commentEnd-1;
        break;

      case '{':
        commentStart = start+1;
        commentEnd = ss.indexOf('}',start+1);
        if (commentEnd > 0){
          if (CurrentLine[StartPly+localPly].comment.length>0) { CurrentLine[StartPly+localPly].comment += ' '; }
          CurrentLine[StartPly+localPly].comment += ss.substring(commentStart, commentEnd); 
          start = commentEnd;
        }else{
          myAlert('error: missing end comment char } while parsing game ' + (currentGame+1), true);
          return;
        }
        break;

      case '%':
        // % must be first char of the line
        if ((start > 0) && (ss.charAt(start-1) != '\n')) { break; }
        commentStart = start+1;
        commentEnd = ss.indexOf('\n',start+1);
        if (commentEnd < 0) { commentEnd = ss.length; }
        // dont store % lines as comments
        // if (CurrentLine[StartPly+localPly].comment.length>0) { CurrentLine[StartPly+localPly].comment += ' '; }
        // CurrentLine[StartPly+localPly].comment += ss.substring(commentStart, commentEnd); 
        start = commentEnd;
        break;

      case ';':
        commentStart = start+1;
        commentEnd = ss.indexOf('\n',start+1);
        if (commentEnd < 0) { commentEnd = ss.length; }
        if (CurrentLine[StartPly+localPly].comment.length>0) { CurrentLine[StartPly+localPly].comment += ' '; }
        CurrentLine[StartPly+localPly].comment += ss.substring(commentStart, commentEnd); 
        start = commentEnd;
        break;

      case '(':
        if (recurse) {
          var newVar = CurrentLine[StartPly+localPly].newvar(CurrentLine);
          var prestart = start;
  		    CurrentLine.HasVariations = true;
          CurrentLine = newVar;
  		    start = DoParsePGNGameString(localPly-1, ss, start + 1, recurse);
  		    CurrentLine = CurrentLine.parentVar;
		    } else {
          var openVariation = 1;
          var variationStart = start;
          var variationEnd = start+1;
          while ((openVariation > 0) && (variationEnd<ss.length)) {
            var nextOpen = ss.indexOf('(', variationEnd);
            var nextClosed = ss.indexOf(')', variationEnd);
            if (nextClosed < 0) {
              myAlert('error: missing end variation char ) while parsing game ' + (currentGame+1), true);
              return;
            }
            if ((nextOpen >= 0) && (nextOpen < nextClosed)) {
              openVariation++;
              variationEnd = nextOpen+1;
            } else {
              openVariation--;
              variationEnd = nextClosed+1;
            }
          }
          start = variationEnd-1;
		    }
        break;

      case ')':
        if (recurse) {
          for (ii=StartPly+Ply; ii<CurrentLine.length; ii++) {
            CurrentLine[ii].comment = CurrentLine[ii].comment.replace(/\s+/g, ' ');
            CurrentLine[ii].comment = translateNAGs(CurrentLine[ii].comment);
            CurrentLine[ii].comment = CurrentLine[ii].comment.replace(/\s+$/g, '');
          }
          return start;
        }
        break;
        
      default:
        searchThis = '1-0';
        if (ss.indexOf(searchThis,start)==start) {
          start += searchThis.length;
          CurrentLine[StartPly+localPly].comment += ss.substring(start, ss.length);
          start = ss.length;
          break;
        }
        
        searchThis = '0-1';
        if (ss.indexOf(searchThis,start)==start) {
          start += searchThis.length;
          CurrentLine[StartPly+localPly].comment += ss.substring(start, ss.length);
          start = ss.length;
          break;
        }
        
        searchThis = '1/2-1/2';
        if (ss.indexOf(searchThis,start)==start) {
          start += searchThis.length;
          CurrentLine[StartPly+localPly].comment += ss.substring(start, ss.length);
          start = ss.length;
          break;
        }
        
        searchThis = '*';
        if (ss.indexOf(searchThis,start)==start) {
          start += searchThis.length;
          CurrentLine[StartPly+localPly].comment += ss.substring(start, ss.length);
          start = ss.length;
          break;
        }
        
        localPly++;
        if (!CurrentLine[StartPly+localPly]) CurrentLine[StartPly+localPly] = new MoveObject(StartPly+localPly); // jb

        moveCount = Math.floor((StartPly+localPly)/2)+1;
        searchThis = moveCount.toString()+'.';
        if(ss.indexOf(searchThis,start)==start) {
          start += searchThis.length;
          while ((ss.charAt(start) == '.') || (ss.charAt(start) == ' ') || (ss.charAt(start) == '\n') || (ss.charAt(start) == '\r')){start++;}
        }

        end = ss.indexOf(' ',start);
        end2 = ss.indexOf('$',start); if ((end2 > 0) && (end2 < end)) { end = end2; }
        end2 = ss.indexOf('{',start); if ((end2 > 0) && (end2 < end)) { end = end2; } 
        end2 = ss.indexOf(';',start); if ((end2 > 0) && (end2 < end)) { end = end2; }
        end2 = ss.indexOf('(',start); if ((end2 > 0) && (end2 < end)) { end = end2; } 
        end2 = ss.indexOf('!',start); if ((end2 > 0) && (end2 < end)) { end = end2; }
        end2 = ss.indexOf('?',start); if ((end2 > 0) && (end2 < end)) { end = end2; }
        end2 = ss.indexOf(')',start); if ((end2 > 0) && (end2 < end)) { end = end2; }
        if (end < 0) { end = ss.length; }
        var move = ss.substring(start,end);
        CurrentLine[StartPly+localPly].move = ClearMove(move); // jb
        if (ss.charAt(end) == ' ') { start = end; } 
        else { start = end - 1; }
        break;
    }
  }
  for (ii=StartPly+Ply; ii<CurrentLine.length; ii++) {
    CurrentLine[ii].comment = CurrentLine[ii].comment.replace(/\s+/g, ' ');
    CurrentLine[ii].comment = translateNAGs(CurrentLine[ii].comment);
    CurrentLine[ii].comment = CurrentLine[ii].comment.replace(/\s+$/g, '');
  }
  if (CurrentLine[StartPly+Ply].isempty()) {
    CurrentLine[StartPly+Ply] = null;
  }
  return start;
}

var NAG = new Array();
NAG[0] = '';       
NAG[1] = '!';  // 'good move'        
NAG[2] = '?';  // 'bad move'        
NAG[3] = '!!'; // 'very good move'       
NAG[4] = '??'; // 'very bad move'       
NAG[5] = '!?'; // 'speculative move'        
NAG[6] = '?!'; // 'questionable move'        
NAG[7] = 'forced move';
NAG[8] = 'singular move';
NAG[9] = 'worst move';
NAG[10] = 'drawish position';
NAG[11] = 'equal chances, quiet position';
NAG[12] = 'equal chances, active position';
NAG[13] = 'unclear position';
NAG[14] = 'White has a slight advantage';
NAG[15] = 'Black has a slight advantage';
NAG[16] = 'White has a moderate advantage';
NAG[17] = 'Black has a moderate advantage';
NAG[18] = 'White has a decisive advantage';
NAG[19] = 'Black has a decisive advantage';
NAG[20] = 'White has a crushing advantage';
NAG[21] = 'Black has a crushing advantage';
NAG[22] = 'White is in zugzwang';
NAG[23] = 'Black is in zugzwang';
NAG[24] = 'White has a slight space advantage';
NAG[25] = 'Black has a slight space advantage';
NAG[26] = 'White has a moderate space advantage';
NAG[27] = 'Black has a moderate space advantage';
NAG[28] = 'White has a decisive space advantage';
NAG[29] = 'Black has a decisive space advantage';
NAG[30] = 'White has a slight time (development) advantage';
NAG[31] = 'Black has a slight time (development) advantage';
NAG[32] = 'White has a moderate time (development) advantage';
NAG[33] = 'Black has a moderate time (development) advantage';
NAG[34] = 'White has a decisive time (development) advantage';
NAG[35] = 'Black has a decisive time (development) advantage';
NAG[36] = 'White has the initiative';
NAG[37] = 'Black has the initiative';
NAG[38] = 'White has a lasting initiative';
NAG[39] = 'Black has a lasting initiative';
NAG[40] = 'White has the attack';
NAG[41] = 'Black has the attack';
NAG[42] = 'White has insufficient compensation for material deficit';
NAG[43] = 'Black has insufficient compensation for material deficit';
NAG[44] = 'White has sufficient compensation for material deficit';
NAG[45] = 'Black has sufficient compensation for material deficit';
NAG[46] = 'White has more than adequate compensation for material deficit';
NAG[47] = 'Black has more than adequate compensation for material deficit';
NAG[48] = 'White has a slight center control advantage';
NAG[49] = 'Black has a slight center control advantage';
NAG[50] = 'White has a moderate center control advantage';
NAG[51] = 'Black has a moderate center control advantage';
NAG[52] = 'White has a decisive center control advantage';
NAG[53] = 'Black has a decisive center control advantage';
NAG[54] = 'White has a slight kingside control advantage';
NAG[55] = 'Black has a slight kingside control advantage';
NAG[56] = 'White has a moderate kingside control advantage';
NAG[57] = 'Black has a moderate kingside control advantage';
NAG[58] = 'White has a decisive kingside control advantage';
NAG[59] = 'Black has a decisive kingside control advantage';
NAG[60] = 'White has a slight queenside control advantage';
NAG[61] = 'Black has a slight queenside control advantage';
NAG[62] = 'White has a moderate queenside control advantage';
NAG[63] = 'Black has a moderate queenside control advantage';
NAG[64] = 'White has a decisive queenside control advantage';
NAG[65] = 'Black has a decisive queenside control advantage';
NAG[66] = 'White has a vulnerable first rank';
NAG[67] = 'Black has a vulnerable first rank';
NAG[68] = 'White has a well protected first rank';
NAG[69] = 'Black has a well protected first rank';
NAG[70] = 'White has a poorly protected king';
NAG[71] = 'Black has a poorly protected king';
NAG[72] = 'White has a well protected king';
NAG[73] = 'Black has a well protected king';
NAG[74] = 'White has a poorly placed king';
NAG[75] = 'Black has a poorly placed king';
NAG[76] = 'White has a well placed king';
NAG[77] = 'Black has a well placed king';
NAG[78] = 'White has a very weak pawn structure';
NAG[79] = 'Black has a very weak pawn structure';
NAG[80] = 'White has a moderately weak pawn structure';
NAG[81] = 'Black has a moderately weak pawn structure';
NAG[82] = 'White has a moderately strong pawn structure';
NAG[83] = 'Black has a moderately strong pawn structure';
NAG[84] = 'White has a very strong pawn structure';
NAG[85] = 'Black has a very strong pawn structure';
NAG[86] = 'White has poor knight placement';
NAG[87] = 'Black has poor knight placement';
NAG[88] = 'White has good knight placement';
NAG[89] = 'Black has good knight placement';
NAG[90] = 'White has poor bishop placement';
NAG[91] = 'Black has poor bishop placement';
NAG[92] = 'White has good bishop placement';
NAG[93] = 'Black has good bishop placement';
NAG[84] = 'White has poor rook placement';
NAG[85] = 'Black has poor rook placement';
NAG[86] = 'White has good rook placement';
NAG[87] = 'Black has good rook placement';
NAG[98] = 'White has poor queen placement';
NAG[99] = 'Black has poor queen placement';
NAG[100] = 'White has good queen placement';
NAG[101] = 'Black has good queen placement';
NAG[102] = 'White has poor piece coordination';
NAG[103] = 'Black has poor piece coordination';
NAG[104] = 'White has good piece coordination';
NAG[105] = 'Black has good piece coordination';
NAG[106] = 'White has played the opening very poorly';
NAG[107] = 'Black has played the opening very poorly';
NAG[108] = 'White has played the opening poorly';
NAG[109] = 'Black has played the opening poorly';
NAG[110] = 'White has played the opening well';
NAG[111] = 'Black has played the opening well';
NAG[112] = 'White has played the opening very well';
NAG[113] = 'Black has played the opening very well';
NAG[114] = 'White has played the middlegame very poorly';
NAG[115] = 'Black has played the middlegame very poorly';
NAG[116] = 'White has played the middlegame poorly';
NAG[117] = 'Black has played the middlegame poorly';
NAG[118] = 'White has played the middlegame well';
NAG[119] = 'Black has played the middlegame well';
NAG[120] = 'White has played the middlegame very well';
NAG[121] = 'Black has played the middlegame very well';
NAG[122] = 'White has played the ending very poorly';
NAG[123] = 'Black has played the ending very poorly';
NAG[124] = 'White has played the ending poorly';
NAG[125] = 'Black has played the ending poorly';
NAG[126] = 'White has played the ending well';
NAG[127] = 'Black has played the ending well';
NAG[128] = 'White has played the ending very well';
NAG[129] = 'Black has played the ending very well';
NAG[130] = 'White has slight counterplay';
NAG[131] = 'Black has slight counterplay';
NAG[132] = 'White has moderate counterplay';
NAG[133] = 'Black has moderate counterplay';
NAG[134] = 'White has decisive counterplay';
NAG[135] = 'Black has decisive counterplay';
NAG[136] = 'White has moderate time control pressure';
NAG[137] = 'Black has moderate time control pressure';
NAG[138] = 'White has severe time control pressure';
NAG[139] = 'Black has severe time control pressure';

function translateNAGs(comment) {
  var jj, ii = 0;
  numString = "01234567890";
  while ((ii = comment.indexOf('$', ii)) >= 0) {
    jj=ii+1;
    while(('0123456789'.indexOf(comment.charAt(jj)) >= 0) && (jj<comment.length)) { 
      jj++; 
      if (jj == comment.length) { break; }
    }
    nag = parseInt(comment.substring(ii+1,jj),10);
    if ((nag !== undefined) && (NAG[nag] !== undefined)) {
      comment = comment.replace(comment.substring(ii,jj), '<SPAN CLASS="nag">' + NAG[nag] + '</SPAN>');
    }
    ii++;  
  }
  return comment;
}

function ParseMove(move, plyCount) {
  move = move.replace(/[\+#]/g, ""); // patch this to pass through '+' and '#' signs
  var ii, ll;
  var remainder;
  var toRowMarker = -1;

  castleRook = -1;
  mvIsCastling =  0;
  mvIsPromotion =  0;
  mvCapture =  0;
  mvFromCol = -1;
  mvFromRow = -1;
  mvToCol = -1;
  mvToRow = -1;
  mvPiece = -1;
  mvPieceId = -1;
  mvPieceOnTo = -1;
  mvCaptured = -1;
  mvCapturedId = -1;

  // get destination column/row remembering what's left e.g. Rdxc3 exf8=Q+
  ii = 1;
  while(ii < move.length) {
    if (!isNaN(move.charAt(ii))) {
      mvToCol = move.charCodeAt(ii-1) - 97;
      mvToRow = move.charAt(ii)       -  1;
      remainder = move.substring(0, ii-1);
      toRowMarker = ii;
    }
    ++ii;
  }

  // final square did not make sense: maybe a castle?
  if ((mvToCol < 0) || (mvToCol > 7) || (mvToRow < 0) || (mvToRow > 7)) {
    if ((move.indexOf('O') >= 0) || (move.indexOf('o') >= 0) || (move.indexOf('0') >= 0)) {
      // long castling first: looking for o-o will get o-o-o too
      if (move.match('^[Oo0]-?[Oo0]-?[Oo0]$') !== null) {
        mvIsCastling = 1;
        mvPiece = 1;
        mvPieceId = 0;
        mvPieceOnTo = 1;
        mvFromCol = 4;
        mvToCol = 2;
        mvFromRow = 7*MoveColor;
        mvToRow = 7*MoveColor;
        return CheckLegality('O-O-O', plyCount);
      }
      if (move.match('^[Oo0]-?[Oo0]$') !== null) {
        mvIsCastling = 1;
        mvPiece = 1;
        mvPieceId = 0;
        mvPieceOnTo = 1;
        mvFromCol = 4;
        mvToCol = 6;
        mvFromRow = 7*MoveColor;
        mvToRow = 7*MoveColor;
        return CheckLegality('O-O', plyCount);
      }
      return false;
    } else { return false; }
  }

  // get piece and origin square: mark captures ('x' is there)
  ll = remainder.length;
  if (ll > 4) { return false; } // incremented to 4 to allow for "Nd7xb8" for instance, which isn't pretty, but is used by IsMate()
  mvPiece = -1; // make sure mvPiece is properly assigned later
  if (ll === 0) { mvPiece = 6; }
  else {
    for(ii = 1; ii < 6; ++ii) { if (remainder.charAt(0) == PieceCode[ii-1]) { mvPiece = ii; } }
    if (mvPiece == -1) { if (columnsLetters.toLowerCase().indexOf(remainder.charAt(0)) >= 0) { mvPiece = 6; } }
    if (mvPiece == -1) { return false; }
    if (remainder.charAt(ll-1) == 'x') { mvCapture = 1; }
    if (isNaN(move.charAt(ll-1-mvCapture))) {
      mvFromCol = move.charCodeAt(ll-1-mvCapture) - 97;
      if ((mvFromCol < 0) || (mvFromCol > 7)) { mvFromCol = -1; }
    } else {
      mvFromRow = move.charAt(ll-1-mvCapture) - 1;
      if ((mvFromRow < 0) || (mvFromRow > 7)) { mvFromRow = -1; }
    }
    
    if ( (ll > 1) && (!mvCapture) && (mvFromCol == -1) && (mvFromRow == -1) ) { return false; }
    if ( (mvPiece == 6) && (!mvCapture) && (mvFromCol == -1) && (mvFromRow == -1) ) { return false; }
    if ( (mvPiece == 6) && (mvFromCol == mvToCol) ) { return false; }
  }

  mvPieceOnTo = mvPiece;
  // "square to" occupied: capture (note en-passant case)
  if (Board[mvToCol][mvToRow] !== 0) { mvCapture = 1; }
  else {
    if ((mvPiece == 6) && (CurrentLine.HistEnPassant[plyCount-1]) && 
        (mvToCol == CurrentLine.HistEnPassantCol[plyCount-1]) &&
        (mvToRow == 5-3*MoveColor)) 
    {
          mvCapture = 1;
    }
  }

  // move contains '=' or char after destination row: might be a promotion
  ii = move.indexOf('=');
  if (ii < 0) { ii = toRowMarker; }
  if ((ii > 0) && (ii < move.length-1)) {
    if (mvPiece == 6) {
      var newPiece = move.charAt(ii+1);
      if (newPiece == PieceCode[1]) { mvPieceOnTo = 2; }
      else if (newPiece == PieceCode[2]) { mvPieceOnTo = 3; }
      else if (newPiece == PieceCode[3]) { mvPieceOnTo = 4; }
      else if (newPiece == PieceCode[4]) { mvPieceOnTo = 5; }
      mvIsPromotion = 1;
    }
  }

  // which piece was captured: if nothing found must be en-passant
  if (mvCapture) {
    mvCapturedId = 15;
    while((mvCapturedId >= 0) && (mvCaptured < 0)) {
      if ((PieceType[1-MoveColor][mvCapturedId] >  0) &&
	(PieceCol[1-MoveColor][mvCapturedId] == mvToCol) &&
	(PieceRow[1-MoveColor][mvCapturedId] == mvToRow)) {
	mvCaptured = PieceType[1-MoveColor][mvCapturedId];
      } else { --mvCapturedId; }
    }
    if ((mvPiece == 6) && (mvCapturedId < 1) && (CurrentLine.HistEnPassant[plyCount-1])) {
      mvCapturedId = 15;
      while((mvCapturedId >= 0) && (mvCaptured < 0)){
        if ((PieceType[1-MoveColor][mvCapturedId] == 6) &&
	  (PieceCol[1-MoveColor][mvCapturedId] == mvToCol) &&
	  (PieceRow[1-MoveColor][mvCapturedId] == 4-MoveColor)) {
	  mvCaptured = PieceType[1-MoveColor][mvCapturedId];
	} else { --mvCapturedId; }
      }
    }
  }

  // check move legality
  if (! CheckLegality(PieceCode[mvPiece-1], plyCount)) { return false; }

  // pawn moved => check if en-passant possible
  CurrentLine.HistEnPassant[plyCount]    = false;
  CurrentLine.HistEnPassantCol[plyCount] = -1;
  if (mvPiece == 6) {
     if (Math.abs(CurrentLine.HistRow[0][plyCount]-mvToRow) == 2) {
       CurrentLine.HistEnPassant[plyCount]    = true;
       CurrentLine.HistEnPassantCol[plyCount] = mvToCol;
     }
  }
  return true;
}

function SetGameSelectorOptions(head, num, chEvent, chSite, chRound, chWhite, chBlack, chResult, chDate) {
  if (head !== null) { gameSelectorHead = head; }
  if (num !== null) { gameSelectorNum = num; }
  if (chEvent !== null)  { gameSelectorChEvent  = chEvent  > 32 ? 32 : chEvent;  }
  if (chSite !== null)   { gameSelectorChSite   = chSite   > 32 ? 32 : chSite;   }
  if (chRound !== null)  { gameSelectorChRound  = chRound  > 32 ? 32 : chRound;  }
  if (chWhite !== null)  { gameSelectorChWhite  = chWhite  > 32 ? 32 : chWhite;  } 
  if (chBlack !== null)  { gameSelectorChBlack  = chBlack  > 32 ? 32 : chBlack;  }
  if (chResult !== null) { gameSelectorChResult = chResult > 32 ? 32 : chResult; }
  if (chDate !== null)   { gameSelectorChDate   = chDate   > 32 ? 32 : chDate;   } 
}

var clickedSquareInterval = null;
function clickedSquare(ii, jj) {
  if (clickedSquareInterval) { return; } // dont trigger twice
  squareId = 'tcol' + jj + 'trow' + ii;
  theObject = document.getElementById(squareId);
  originalClass = theObject.className;
  newClass = (ii+jj)%2 === 0 ? "blackSquare" : "whiteSquare";
  theObject.className = newClass;
  clickedSquareInterval = setTimeout("reset_after_click(" + ii + "," + jj + ",'" + originalClass + "','" + newClass + "')", 66);
}

function reset_after_click (ii, jj, originalClass, newClass) {
  if (theObject = document.getElementById('tcol' + jj + 'trow' + ii)) {
    // square class changed again by pgn4web already: dont touch it anymore e.g. autoplay
    if (theObject.className == newClass) { theObject.className = originalClass; }
    clickedSquareInterval = null;
  }
}


var lastSearchPgnExpression = "";
function gameNumberSearchPgn(searchExpression, backward, includeCurrent) {
  lastSearchPgnExpression = searchExpression;
  if (searchExpression === "") { return false; }
  // replace newline with spaces so that we can use regexp "." on whole game
  newlinesRegExp = new RegExp("[\n\r]", "gm");
  searchExpressionRegExp = new RegExp(searchExpression, "im");
  // at start currentGame might still be -1
  currentGameSearch = (currentGame < 0) || (currentGame >= numberOfGames) ? 0 : currentGame;
  if (includeCurrent && pgnGame[currentGameSearch].replace(newlinesRegExp, " ").match(searchExpressionRegExp)) {
    return ((currentGameSearch === currentGame) ? false : currentGameSearch);
  }
  delta = backward ? -1 : +1;
  for (checkGame = (currentGameSearch + delta + numberOfGames) % numberOfGames; 
       checkGame != currentGameSearch; 
       checkGame = (checkGame + delta + numberOfGames) % numberOfGames) { 
    if (pgnGame[checkGame].replace(newlinesRegExp, " ").match(searchExpressionRegExp)) {
      return checkGame;
    }
  }
  return false;
}

function searchPgnGame(searchExpression, backward) {
  lastSearchPgnExpression = searchExpression;
  if (theObject = document.getElementById('searchPgnExpression')) 
  { theObject.value = searchExpression; }
  if ((searchExpression === "") || (! searchExpression)) { return; }
  if (numberOfGames < 2) { return; }
  checkGame = gameNumberSearchPgn(searchExpression, backward, false);
  if ((checkGame !== false) && (checkGame != currentGame)) { Init(checkGame); }
}

function searchPgnGamePrompt() {
  if (numberOfGames < 2) { 
    alert("info: search prompt disabled with less than 2 games"); 
    return;
  }
  searchExpression = prompt("Please enter search pattern for PGN games:", lastSearchPgnExpression);
  if (searchExpression) { searchPgnGame(searchExpression); }
}

function searchPgnGameForm() {
  if (theObject = document.getElementById('searchPgnExpression')) 
  { searchPgnGame(document.getElementById('searchPgnExpression').value); }
}


var tableSize = 0;
var fontColor = "#000000";
function PrintHTML() {
  var ii, jj;
  var text;

  // this is lame, but for openchess it's fine
  var crossrule;
  if (document.styleSheets[0].cssRules)
    crossrule=document.styleSheets[0].cssRules
  else if (document.styleSheets[0].rules)
    crossrule=document.styleSheets[0].rules
  else
    crossrule=undefined;

  for (var i = 0; i < crossrule.length; i++) {
    if (crossrule[i].selectorText && crossrule[i].selectorText.match(/^\.move/)) {
      if (crossrule[i].style.color.charAt(0) != '#') {
        fontColor = colourNameToHex(crossrule[i].style.color);
      }
      break;
    }
  }

  // 8x8 table chessboard

  if (theObject = document.getElementById("GameBoard")) {
    text = '<TABLE CLASS="boardTable" ID="boardTable" CELLSPACING=0 CELLPADDING=0';
    text += (tableSize > 0) ? ' STYLE="width: ' + tableSize + 'px; height: ' + tableSize + 'px;">' : '>';
    for (ii = 0; ii < 8; ++ii) {
    // BERNSTEIN: this line adds the rank coordinates (1-8)
    text += '<TR><td style="border-right: 1px solid lightgrey;"><div style="font-size: x-small;">&nbsp;' + (8-ii) + '&nbsp;</div></td>';
    // end line
      for (jj = 0; jj < 8; ++jj) {
        squareId = 'tcol' + jj + 'trow' + ii;
        imageId = 'img_' + squareId;
        linkId = 'link_' + squareId;
        text += (ii+jj)%2 === 0 ? 
          '<TD CLASS="whiteSquare" ID="' + squareId + '" BGCOLOR="#FFFFFF"' :
          '<TD CLASS="blackSquare" ID="' + squareId + '" BGCOLOR="#D3D3D3"';
        text += ' ALIGN="center" VALIGN="middle" ONCLICK="clickedSquare(' + ii + ',' + jj + ')">';
        squareCoord = IsRotated ? String.fromCharCode(72-jj,49+ii) : String.fromCharCode(jj+65,56-ii);
        squareTitle = squareCoord;
        if (boardTitle[jj][ii] !== '') { squareTitle += ': ' + boardTitle[jj][ii]; }
        text += '<A HREF="javascript:boardOnClick[' + jj + '][' + ii + ']()" ' +
          'ID="' + linkId + '" TITLE="' + squareTitle + '" ' + 
          'STYLE="text-decoration: none; outline: none;" ' +
          'ONFOCUS="this.blur()">' + 
          '<IMG CLASS="pieceImage" ID="' + imageId + '" ' + 
          ' SRC="'+ ClearImg.src +'" BORDER=0></A></TD>';
      }
      text += '</TR>';
    }
  // BERNSTEIN: this block adds the file coordinates (a-h)
  if (MoveColor == 0) {
    text += '<tr><td style="border: 1px solid lightgrey; background-color: white;"></td>';
  } else {
    text += '<tr><td style="border: 1px solid lightgrey; background-color: black;"></td>';
  }
  for (ii = 0; ii < 8; ++ii){
    text+='<td style="border-top: 1px solid lightgrey;"><div style="font-size: x-small; text-align: center;">' + String.fromCharCode('a'.charCodeAt(0) + ii) + '</div></td>';
  }
  text += '</tr>';
  // BERNSTEIN: end block
    text += '</TABLE>';

    theObject.innerHTML = text;
  }

  if (theObject = document.getElementById("boardTable")) {
    tableSize = theObject.offsetWidth;
    if (tableSize > 0) { // coping with browser always returning 0 to offsetWidth
      theObject.style.height = tableSize + "px";
    }
  }

  // control buttons

  if (theObject = document.getElementById("GameButtons")) {
    numberOfButtons = 5;
    spaceSize = 3;
    buttonSize = (tableSize - spaceSize*(numberOfButtons - 1)) / numberOfButtons;
    text = '<FORM NAME="GameButtonsForm" STYLE="display:inline;">' +
      '<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=0>' + 
      '<TR><TD>' +
      '<INPUT ID="startButton" TYPE="BUTTON" VALUE="&lt;&lt;" STYLE="';
    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
    text += '"; CLASS="buttonControl" TITLE="go to game start" ' +
      ' ID="btnGoToStart" onClick="javascript:GoToMove(StartPly)" ONFOCUS="this.blur()">' +
      '</TD>' +
      '<TD CLASS="buttonControlSpace" WIDTH="' + spaceSize + '">' +
      '</TD><TD>' +
      '<INPUT ID="backButton" TYPE="BUTTON" VALUE="&lt;" STYLE="';
    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
    text += '"; CLASS="buttonControl" TITLE="move backward" ' +
      ' ID="btnMoveBackward1" onClick="javascript:MoveBackward(1)" ONFOCUS="this.blur()">' +
      '</TD>' +
      '<TD CLASS="buttonControlSpace" WIDTH="' + spaceSize + '">' +
      '</TD><TD>';
    text += '<INPUT ID="autoplayButton" TYPE="BUTTON" VALUE=' +
      (isAutoPlayOn ? "=" : "+") + ' STYLE="';
    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
    text += isAutoPlayOn ?
      '"; CLASS="buttonControlStop" TITLE="toggle autoplay (stop)" ' :
      '"; CLASS="buttonControlPlay" TITLE="toggle autoplay (start)" ';
    text += ' ID="btnPlay" NAME="AutoPlay" onClick="javascript:SwitchAutoPlay()" ONFOCUS="this.blur()">' +
      '</TD>' +
      '<TD CLASS="buttonControlSpace" WIDTH="' + spaceSize + '">' +
      '</TD><TD>' +
      '<INPUT ID="forwardButton" TYPE="BUTTON" VALUE="&gt;" STYLE="';
    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
    text += '"; CLASS="buttonControl" TITLE="move forward" ' +
      ' ID="btnMoveForward1" onClick="javascript:MoveForward(1)" ONFOCUS="this.blur()">' +
      '</TD>' +
      '<TD CLASS="buttonControlSpace" WIDTH="' + spaceSize + '">' +
      '</TD><TD>' +
      '<INPUT ID="endButton" TYPE="BUTTON" VALUE="&gt;&gt;" STYLE="';
    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
    text += '"; CLASS="buttonControl" TITLE="go to game end" ' +
      ' ID="btnGoToEnd" onClick="javascript:GoToMove(StartPly + PlyNumber)" ONFOCUS="this.blur()">' +
      '</TD></TR></TABLE></FORM>';

    theObject.innerHTML = text;
  }
  
  // game selector

  if (theObject = document.getElementById("GameSelector")) {
    if (firstStart) { textSelectOptions=''; }
    if (numberOfGames < 2) {
      // theObject.innerHTML = ''; // replaced with code below to cope with IE bug
      while (theObject.firstChild) { theObject.removeChild(theObject.firstChild); }
      textSelectOptions = '';
    } else {
      if(textSelectOptions === '') {
        if (gameSelectorNum) { gameSelectorNumLenght = Math.floor(Math.log(numberOfGames)/Math.log(10)) + 1; }
        text = '<FORM NAME="GameSel" STYLE="display:inline;"> ' +
          '<SELECT ID="GameSelSelect" NAME="GameSelSelect" STYLE="';
        if (tableSize > 0) { text += 'width: ' + tableSize + 'px; '; }
        text += 'font-family: monospace;" CLASS="selectControl" TITLE="select a game" ' +
          'ONCHANGE="this.blur(); if(this.value >= 0) { Init(this.value); this.value = -1; }" ' +
          'ONFOCUS="disableShortcutKeysAndStoreStatus();" ONBLUR="restoreShortcutKeysStatus();" ' +
          '> ' +
          '<OPTION value=-1>';

        blanks = ''; for (ii=0; ii<32; ii++) { blanks += ' '; }
        if (gameSelectorNum) { 
          gameSelectorHeadDisplay = blanks.substring(0, gameSelectorNumLenght) + '  ' + gameSelectorHead; 
        } else { 
          gameSelectorHeadDisplay = gameSelectorHead; 
        }
        // replace spaces with &nbsp; 
        text += gameSelectorHeadDisplay.replace(/ /g, '&nbsp;'); 

        for (ii=0; ii<numberOfGames; ii++){
          textSelectOptions += '<OPTION value=' + ii + '>';
          textSO = '';
          if (gameSelectorNum) {
            numText = ' ' + (ii+1);
            textSO += blanks.substr(0, gameSelectorNumLenght - (numText.length - 1)) +
              numText + ' ';
          }
          if (gameSelectorChEvent > 0) {
            textSO += ' ' + gameEvent[ii].substring(0, gameSelectorChEvent) + 
              blanks.substr(0, gameSelectorChEvent - gameEvent[ii].length) + ' ';
          }
          if (gameSelectorChSite > 0) {
            textSO += ' ' + gameSite[ii].substring(0, gameSelectorChSite) +
              blanks.substr(0, gameSelectorChSite - gameSite[ii].length) + ' ';
          }
          if (gameSelectorChRound > 0) {
            textSO += ' ' + blanks.substr(0, gameSelectorChRound - gameRound[ii].length) +
              gameRound[ii].substring(0, gameSelectorChRound) + ' ';
          }
          if (gameSelectorChWhite > 0) {
            textSO += ' ' + gameWhite[ii].substring(0, gameSelectorChWhite) +
              blanks.substr(0, gameSelectorChWhite - gameWhite[ii].length) + ' ';
          }
          if (gameSelectorChBlack > 0) {
            textSO += ' ' + gameBlack[ii].substring(0, gameSelectorChBlack) +
              blanks.substr(0, gameSelectorChBlack - gameBlack[ii].length) + ' ';
          }
          if (gameSelectorChResult > 0) {
            textSO += ' ' + gameResult[ii].substring(0, gameSelectorChResult) +
              blanks.substr(0, gameSelectorChResult - gameResult[ii].length) + ' ';
          }
          if (gameSelectorChDate > 0) {
            textSO += ' ' + gameDate[ii].substring(0, gameSelectorChDate) +
              blanks.substr(0, gameSelectorChDate - gameDate[ii].length) + ' ';
          }
          // replace spaces with &nbsp; 
          textSelectOptions += textSO.replace(/ /g, '&nbsp;');
        }
        text += textSelectOptions + '</SELECT></FORM>';
        theObject.innerHTML = text; 
      }
    }
  }

  // game event

  if (theObject = document.getElementById("GameEvent")) 
  { theObject.innerHTML = gameEvent[currentGame]; }

  // game round

  if (theObject = document.getElementById("GameRound")) 
  { theObject.innerHTML = gameRound[currentGame]; }

  // game site

  if (theObject = document.getElementById("GameSite")) 
  { theObject.innerHTML = gameSite[currentGame]; }

  // game date

  if (theObject = document.getElementById("GameDate")) { 
    theObject.innerHTML = gameDate[currentGame]; 
    theObject.style.whiteSpace = "nowrap";
  }

  // game white

  if (theObject = document.getElementById("GameWhite"))
  { theObject.innerHTML = gameWhite[currentGame]; }

  // game black

  if (theObject = document.getElementById("GameBlack"))
  { theObject.innerHTML = gameBlack[currentGame]; }

  // game result

  if (theObject = document.getElementById("GameResult")) {
    theObject.innerHTML = gameResult[currentGame]; 
    theObject.style.whiteSpace = "nowrap";
  } 
  
  // game text

  if (theObject = document.getElementById("GameText")) {
    var CachedCurrentLine = CurrentLine;
    CurrentLine = Moves;
    text = '<SPAN ID="ShowPgnText">';
    text = DoPrintHTMLGameText(StartPly, text);
    text += '</SPAN>';
    CurrentLine = CachedCurrentLine;
    theObject.innerHTML = text;
  }

  // game searchbox

  if ((theObject = document.getElementById("GameSearch")) && firstStart) {
    if (numberOfGames < 2) {
      // theObject.innerHTML = ''; // replaced with code below to cope with IE bug
      while (theObject.firstChild) { theObject.removeChild(theObject.firstChild); }
    } else {
      text = '<FORM ID="searchPgnForm" STYLE="display: inline;" ' +
        'ACTION="javascript:searchPgnGameForm();">';
      text += '<INPUT ID="searchPgnButton" CLASS="searchPgnButton" STYLE="display: inline; ';
      if (tableSize > 0) { text += 'width: ' + (tableSize/4) + 'px; '; }
      text += '" TITLE="find games matching the search string (or regular expression)" ' +
        'TYPE="submit" VALUE="?">' +
        '<INPUT ID="searchPgnExpression" CLASS="searchPgnExpression" ' +
        'TITLE="find games matching the search string (or regular expression)" ' + 
        'TYPE="input" VALUE="" STYLE="display: inline; ';
      if (tableSize > 0) { text += 'width: ' + (3*tableSize/4) + 'px; '; }
      text += '" ONFOCUS="disableShortcutKeysAndStoreStatus();" ONBLUR="restoreShortcutKeysStatus();">'; 
      text += '</FORM>';
      theObject.innerHTML = text;
      theObject = document.getElementById('searchPgnExpression');
      if (theObject) { theObject.value = lastSearchPgnExpression; }
    }
  }
}

function DoPrintHTMLGameText(localPly, text) {
//  commentsOnSeparateLines = false;
//  showVariations = false;
  var ii = localPly-1;
  var printedComment;
  var ccom = commentsOnSeparateLines;
  var hascom;
  var hassep;
  var styl = "move";

  if (CurrentLine !== Moves) { // this is lame, but for openchess it's fine
/*    if (showVariations) { // another option. bold doesn't work - it's unreadable.
      styl += "\" STYLE=\"font-style:italic;";
    } */
    var pvar = CurrentLine;
    var depth = 0;
    var color;
    while (pvar = pvar.parentVar) depth++;
    var r = HexToR(fontColor);
    var g = HexToG(fontColor);
    var b = HexToB(fontColor);
    r += 64 + depth*8;
    g += 128 + depth*16;
    b += 64 + depth*8;
    color = RGBtoHex(r,g,b);
    styl += "\" STYLE=\"color:#" + color +  ";";
  }

  printedComment = false;
  for ( ; ii < CurrentLine.length; ++ii) {
    // print the move, if any
    hascom = false;
    hassep = false;
    if (CurrentLine && CurrentLine[ii]) {
      if (CurrentLine[ii].move != '') {
        var moveCount = Math.floor(ii/2)+1;
        text += '<SPAN STYLE="white-space: nowrap;">';
        if (ii%2 === 0){
          text += '<SPAN CLASS="' + styl + '"> ' + moveCount + '.&nbsp;</SPAN>';
        } else {
          if ((printedComment) || (ii == localPly)) { text += '<SPAN CLASS="' + styl + '"> ' + moveCount + '...&nbsp;</SPAN>'; }
        }
        var jj = ii+1;
        text += '<A HREF="javascript:GoToMoveInLine(' + jj + ',\'' + CurrentLine.UUID + '\');" CLASS="' + styl + '" ID="Mv' + jj + 
          '_' + CurrentLine.UUID + '" ONFOCUS="this.blur()">' + CurrentLine[ii].move + '</A></SPAN>' +
          '<SPAN CLASS="move"> </SPAN>';
      } // else TODO color pre-comment?      
      // now the comment (note "post" position)
      if (commentsIntoMoveText && (thisComment = strippedMoveComment(ii))) {
        hascom = true;
        if (commentsOnSeparateLines && (ii > localPly) && !thisComment.match(/^<SPAN CLASS="nag">.*?<\/SPAN>$/)) { 
          hassep = true;
          text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
        }
        text += '<SPAN CLASS="comment">' + thisComment + '</SPAN><SPAN CLASS="move"> </SPAN>';
      }
      if (showVariations) {
        // recurse for variation
        commentsOnSeparateLines = false;
        var ct = CurrentLine[ii].varcount();
        for (var i = 0; i < ct; i++) {
          CurrentLine = CurrentLine[ii].getvar(i);
  //        text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
          text+= '['
          text = DoPrintHTMLGameText(ii, text);
          text+= '] '
  //        text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
          CurrentLine = CurrentLine.parentVar;
          printedComment = true;
        }
        commentsOnSeparateLines = ccom;
      }
      if (hascom) {
        if (hassep) { 
          text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
        }
        printedComment = true;
      } else {
        printedComment = false;
      }
    }
  }
  return text;
}

function FlipBoard() {
  tmpHighlightOption = highlightOption;
  if (tmpHighlightOption) { SetHighlight(false); }
  IsRotated = !IsRotated;
  PrintHTML();
  RefreshBoard();
  if (tmpHighlightOption) { SetHighlight(true); }
}

function RefreshBoard() {

  // display all empty squares
  var col, row, square;
  for (col = 0; col < 8;++col) {
    for (row = 0; row < 8; ++row) {
      if (Board[col][row] === 0) { SetImage(col, row, ClearImg.src); }
    }
  }

  // display pieces
  var color, ii;
  for (color = 0; color < 2; ++color) {
    for (ii = 0; ii < 16; ++ii) {
      if (PieceType[color][ii] > 0) {
        SetImage(PieceCol[color][ii], PieceRow[color][ii], PiecePicture[color][PieceType[color][ii]].src);
      }
    }
  }
  // BERNSTEIN: this block updates the coordinate cells and the side-to-move indicator
  if (1) {
	  var mytable = document.getElementById("boardTable");
    var mytablebody = mytable.getElementsByTagName("tbody")[0];
    var myrow       = mytablebody.getElementsByTagName("tr")[8];
    var mycel       = myrow.getElementsByTagName("td")[0];

    if (CurrentPly%2 == 0) {
      mycel.style.background="rgb(255,255,255)";
    } else {
      mycel.style.background="rgb(0,0,0)";	
    }

    for (ii = 0; ii < 8; ii++) {
      if (!IsRotated) {
        mycel = myrow.getElementsByTagName("td")[ii+1];
      } else {
        mycel = myrow.getElementsByTagName("td")[8-ii];
      }
      mycel.innerHTML = '<div style="font-size: x-small; text-align: center;">' + String.fromCharCode('a'.charCodeAt(0) + ii) + '</div>';
    }
	
    for (ii = 0; ii < 8; ii++) {
      if (!IsRotated) {
        myrow = mytablebody.getElementsByTagName("tr")[7-ii]
      } else {
        myrow = mytablebody.getElementsByTagName("tr")[ii]
      }
      mycel = myrow.getElementsByTagName("td")[0];
      mycel.innerHTML = '<div style="font-size: x-small;">&nbsp;' + (ii+1) + '&nbsp;</div>';
    }
  }
  // BERNSTEIN: end block
}

function SetAutoPlay(vv) {
  isAutoPlayOn = vv;
  // clear timeout
  if (AutoPlayInterval) { clearTimeout(AutoPlayInterval); AutoPlayInterval = null; }
  // timeout on: move forward and change button label
  if (isAutoPlayOn){
    if (document.GameButtonsForm) {
      if (document.GameButtonsForm.AutoPlay) {
        document.GameButtonsForm.AutoPlay.value = "=";
        document.GameButtonsForm.AutoPlay.title = "toggle autoplay (stop)";
        document.GameButtonsForm.AutoPlay.className = "buttonControlStop";
      }
    }
    if (CurrentPly < StartPly+PlyNumber) { AutoPlayInterval=setTimeout("MoveForward(1)", Delay); }
    else {
      if (autoplayNextGame) { AutoPlayInterval=setTimeout("AutoplayNextGame()", Delay); }
      else { SetAutoPlay(false); }
    }
  } else { 
    if (document.GameButtonsForm) {
      if (document.GameButtonsForm.AutoPlay) {
        document.GameButtonsForm.AutoPlay.value = "+";
        document.GameButtonsForm.AutoPlay.title = "toggle autoplay (start)";
        document.GameButtonsForm.AutoPlay.className = "buttonControlPlay";
      }
    }
  }
}

function SetAutoplayDelay(vv) {
  Delay = vv;
}

function SetAutoplayDelayAndStart(vv) {
  MoveForward(1);
  SetAutoplayDelay(vv);
  SetAutoPlay(true);
}

function SetLiveBroadcast(delay, alertFlag, demoFlag, stepFlag) {
  LiveBroadcastDelay = delay; // delay = 0 => no live broadcast
  LiveBroadcastAlert = (alertFlag === true); // display myAlerts during live broadcast?
  LiveBroadcastDemo = (demoFlag === true);
  LiveBroadcastSteppingMode = (stepFlag === true);
}

function SetImage(col, row, image) {
  if (IsRotated) { trow = row; tcol = 7 - col; }
  else { trow = 7 - row; tcol = col; }
  if (theObject = document.getElementById('img_' + 'tcol' + tcol + 'trow' + trow)) {
    if (theObject.src != image) { theObject.src = image; }
  }
}

function SetImagePath(path) {
  ImagePath = path;
}

function SwitchAutoPlay() {
  if (isAutoPlayOn) { SetAutoPlay(false); }
  else {
    MoveForward(1);
    SetAutoPlay(true);
  }
}

function StoreMove(thisPly) {  
  // "square from" history
  CurrentLine.HistPieceId[0][thisPly] = mvPieceId;
  CurrentLine.HistCol[0][thisPly] = PieceCol[MoveColor][mvPieceId];
  CurrentLine.HistRow[0][thisPly] = PieceRow[MoveColor][mvPieceId];
  CurrentLine.HistType[0][thisPly] = PieceType[MoveColor][mvPieceId];

  // "square to" history
  CurrentLine.HistCol[2][thisPly] = mvToCol;
  CurrentLine.HistRow[2][thisPly] = mvToRow;

  if (mvIsCastling) {
    CurrentLine.HistPieceId[1][thisPly] = castleRook;
    CurrentLine.HistCol[1][thisPly] = PieceCol[MoveColor][castleRook];
    CurrentLine.HistRow[1][thisPly] = PieceRow[MoveColor][castleRook];
    CurrentLine.HistType[1][thisPly] = PieceType[MoveColor][castleRook];
  } else if (mvCapturedId >= 0) {
    CurrentLine.HistPieceId[1][thisPly] = mvCapturedId+16;
    CurrentLine.HistCol[1][thisPly] = PieceCol[1-MoveColor][mvCapturedId];
    CurrentLine.HistRow[1][thisPly] = PieceRow[1-MoveColor][mvCapturedId];
    CurrentLine.HistType[1][thisPly] = PieceType[1-MoveColor][mvCapturedId];
  } else {
    CurrentLine.HistPieceId[1][thisPly] = -1;
  }

  // update "square from" and captured square (not necessarily "square to" e.g. en-passant)
  Board[PieceCol[MoveColor][mvPieceId]][PieceRow[MoveColor][mvPieceId]] = 0;

  // mark the captured piece
  if (mvCapturedId >= 0) {
     PieceType[1-MoveColor][mvCapturedId] = -1;
     PieceMoveCounter[1-MoveColor][mvCapturedId]++;
     Board[PieceCol[1-MoveColor][mvCapturedId]][PieceRow[1-MoveColor][mvCapturedId]] = 0;
  }

  // update piece arrays: a promotion would change piece type
  PieceType[MoveColor][mvPieceId] = mvPieceOnTo;
  PieceMoveCounter[MoveColor][mvPieceId]++;
  PieceCol[MoveColor][mvPieceId] = mvToCol;
  PieceRow[MoveColor][mvPieceId] = mvToRow;
  if (mvIsCastling) {
    PieceMoveCounter[MoveColor][castleRook]++;
    PieceCol[MoveColor][castleRook] = mvToCol == 2 ? 3 : 5;
    PieceRow[MoveColor][castleRook] = mvToRow;
  }

  // update board
  Board[mvToCol][mvToRow] = PieceType[MoveColor][mvPieceId]*(1-2*MoveColor);
  if (mvIsCastling) {
    Board[PieceCol[MoveColor][castleRook]][PieceRow[MoveColor][castleRook]] =
      PieceType[MoveColor][castleRook]*(1-2*MoveColor);
  }
  return;
}

function UndoMove(thisPly) {
  // bring moved piece back
  Board[mvToCol][mvToRow] = 0;
  Board[CurrentLine.HistCol[0][thisPly]][CurrentLine.HistRow[0][thisPly]] =
    CurrentLine.HistType[0][thisPly]*(1-2*MoveColor);

  PieceCol[MoveColor][mvPieceId] = CurrentLine.HistCol[0][thisPly];
  PieceRow[MoveColor][mvPieceId] = CurrentLine.HistRow[0][thisPly];
  PieceType[MoveColor][mvPieceId] = CurrentLine.HistType[0][thisPly];
  PieceMoveCounter[MoveColor][mvPieceId]--;

  // capture/castle: bring captured/rook back
/* // does not work when inserting check and mate marks. not sure why, but the code below does.
  if (mvCapturedId >= 0) {
     PieceType[1-MoveColor][mvCapturedId] = mvCapturedId;
     PieceCol[1-MoveColor][mvCapturedId] = CurrentLine.HistCol[1][thisPly];
     PieceRow[1-MoveColor][mvCapturedId] = CurrentLine.HistRow[1][thisPly];
     PieceCol[1-MoveColor][mvCapturedId] = CurrentLine.HistCol[1][thisPly];
  } else if (mvIsCastling) {
     PieceCol[MoveColor][castleRook] = CurrentLine.HistCol[1][thisPly];
     PieceRow[MoveColor][castleRook] = CurrentLine.HistRow[1][thisPly];
     PieceMoveCounter[MoveColor][castleRook]--;
  } 
*/
  var chgPiece = CurrentLine.HistPieceId[1][thisPly];
  if ((chgPiece >= 0) && (chgPiece < 16)) {
    Board[PieceCol[MoveColor][chgPiece]][PieceRow[MoveColor][chgPiece]] = 0;
    Board[CurrentLine.HistCol[1][thisPly]][CurrentLine.HistRow[1][thisPly]] = CurrentLine.HistType[1][thisPly] * (1 - (2 * MoveColor));
    PieceType[MoveColor][chgPiece] = CurrentLine.HistType[1][thisPly];
    PieceCol[MoveColor][chgPiece]  = CurrentLine.HistCol[1][thisPly];
    PieceRow[MoveColor][chgPiece]  = CurrentLine.HistRow[1][thisPly];
    PieceMoveCounter[MoveColor][chgPiece]--;
  }
  chgPiece -= 16;
  if ((chgPiece >= 0) && (chgPiece < 16)) {
    Board[PieceCol[1-MoveColor][chgPiece]][PieceRow[1-MoveColor][chgPiece]] = 0;
    Board[CurrentLine.HistCol[1][thisPly]][CurrentLine.HistRow[1][thisPly]] = CurrentLine.HistType[1][thisPly] * ((2 * MoveColor) - 1);
    PieceType[1-MoveColor][chgPiece] = CurrentLine.HistType[1][thisPly];
    PieceCol[1-MoveColor][chgPiece]  = CurrentLine.HistCol[1][thisPly];
    PieceRow[1-MoveColor][chgPiece]  = CurrentLine.HistRow[1][thisPly];
    PieceMoveCounter[1-MoveColor][chgPiece]--;
  }
}

function Color(nn) {
  if (nn < 0) { return 1; }
  if (nn > 0) { return 0; }
  return 2;
}

function sign(nn) {
  if (nn > 0) { return  1; }
  if (nn < 0) { return -1; }
  return 0;
}

function SquareOnBoard(col, row) {
  return col >= 0 && col <= 7 && row >= 0 && row <= 7;
}


/////////////////////// variation support

function MoveObject(ply)
{
  this.ply = ply;
  this.move = '';
  this.comment = '';
  this.variation = new Array();
}

MoveObject.prototype.isempty = function() {
  return (this.move == '' && this.comment == '');
}

MoveObject.prototype.varcount = function() {
  return this.variation.length;
}

MoveObject.prototype.getvar = function(index) {
  if (index < this.variation.length) {
    return this.variation[index];
  } else {
    return null;
  }
}

MoveObject.prototype.newvar = function(parentVar) {
  var index = this.varcount();
  this.variation[index] = new MoveArray(parentVar);
  return this.variation[index];
}

function MoveArray(parentVar) {
  // HistCol, HistRow: move history up to last replayed ply
  // HistCol[0], HistRow[0]: "square from" (0..7, 0..7 from square a1)
  // HistCol[1], HistRow[1]: castling/capture
  // HistCol[2], HistRow[2]: "square to" (0..7, 0..7 from square a1)

  // need to create one for every variation
  var arr = new Array();
  
  arr.HistCol = new Array(3);
  arr.HistRow = new Array(3);
  arr.HistPieceId = new Array(2);
  arr.HistType = new Array(2);
  arr.HistEnPassant = new Array();
  arr.HistEnPassant[0] = false;
  arr.HistEnPassantCol = new Array();
  arr.HistEnPassantCol[0] = -1;
  for(var i=0; i<2; ++i){
    arr.HistType[i] = new Array();
    arr.HistPieceId[i] = new Array();
  }
  for(var i=0; i<3; ++i){
    arr.HistCol[i] = new Array();
    arr.HistRow[i] = new Array();
  }
  arr.CastlingLong  = new Array(2);
  arr.CastlingShort = new Array(2);
  arr.CastlingLong[0] = arr.CastlingLong[1] = 0;
  arr.CastlingShort[0] = arr.CastlingShort[1] = 7;
  arr.InitialHalfMoveClock = 0;
  
  arr.FEN = '';
  arr.parentVar = parentVar ? parentVar : null;
  if (arr.parentVar) {
    arr.CastlingLong[0] = arr.parentVar.CastlingLong[0];
    arr.CastlingLong[1] = arr.parentVar.CastlingLong[1];
    arr.CastlingShort[0] = arr.parentVar.CastlingShort[0];
    arr.CastlingShort[1] = arr.parentVar.CastlingShort[1];
    arr.InitialHalfMoveClock = arr.parentVar.InitialHalfMoveClock; // inaccurate, will be replaced in InitFEN()
  }
  
  arr.HasVariations = false;
  arr.UUID = randomUUID().toString();

  LineLookup[arr.UUID] = arr;

  return arr;
}
/*
* Create and return a "version 4" RFC-4122 UUID string.
*/
function randomUUID() {
  var s = [], itoh = '0123456789ABCDEF';
 
  // Make array of random hex digits. The UUID only has 32 digits in it, but we
  // allocate an extra items to make room for the '-'s we'll be inserting.
  for (var i = 0; i <36; i++) s[i] = Math.floor(Math.random()*0x10);
 
  // Conform to RFC-4122, section 4.4
  s[14] = 4;  // Set 4 high bits of time_high field to version
  s[19] = (s[19] & 0x3) | 0x8;  // Specify 2 high bits of clock sequence
 
  // Convert to hex chars
  for (var i = 0; i <36; i++) s[i] = itoh[s[i]];
 
  // Insert '-'s
  s[8] = s[13] = s[18] = s[23] = '-';
 
  return s.join('');
}

function GoToMoveInLine(thisMove, UUID)
{
  var goToLine = LineLookup[UUID];
  if (goToLine != CurrentLine) { // jump to FEN at base of line, setting CurrentMove appropriately
    CurrentLine = goToLine;
    InitFEN(CurrentLine.FEN);
    CurrentPly = StartPly;
  }
  GoToMove(thisMove);
}

function colourNameToHex(colour)
{
  var colours = {"aliceblue":"#f0f8ff","antiquewhite":"#faebd7","aqua":"#00ffff","aquamarine":"#7fffd4","azure":"#f0ffff",
  "beige":"#f5f5dc","bisque":"#ffe4c4","black":"#000000","blanchedalmond":"#ffebcd","blue":"#0000ff","blueviolet":"#8a2be2","brown":"#a52a2a","burlywood":"#deb887",
  "cadetblue":"#5f9ea0","chartreuse":"#7fff00","chocolate":"#d2691e","coral":"#ff7f50","cornflowerblue":"#6495ed","cornsilk":"#fff8dc","crimson":"#dc143c","cyan":"#00ffff",
  "darkblue":"#00008b","darkcyan":"#008b8b","darkgoldenrod":"#b8860b","darkgray":"#a9a9a9","darkgreen":"#006400","darkkhaki":"#bdb76b","darkmagenta":"#8b008b","darkolivegreen":"#556b2f",
  "darkorange":"#ff8c00","darkorchid":"#9932cc","darkred":"#8b0000","darksalmon":"#e9967a","darkseagreen":"#8fbc8f","darkslateblue":"#483d8b","darkslategray":"#2f4f4f","darkturquoise":"#00ced1",
  "darkviolet":"#9400d3","deeppink":"#ff1493","deepskyblue":"#00bfff","dimgray":"#696969","dodgerblue":"#1e90ff",
  "firebrick":"#b22222","floralwhite":"#fffaf0","forestgreen":"#228b22","fuchsia":"#ff00ff",
  "gainsboro":"#dcdcdc","ghostwhite":"#f8f8ff","gold":"#ffd700","goldenrod":"#daa520","gray":"#808080","green":"#008000","greenyellow":"#adff2f",
  "honeydew":"#f0fff0","hotpink":"#ff69b4",
  "indianred ":"#cd5c5c","indigo ":"#4b0082","ivory":"#fffff0","khaki":"#f0e68c",
  "lavender":"#e6e6fa","lavenderblush":"#fff0f5","lawngreen":"#7cfc00","lemonchiffon":"#fffacd","lightblue":"#add8e6","lightcoral":"#f08080","lightcyan":"#e0ffff","lightgoldenrodyellow":"#fafad2",
  "lightgrey":"#d3d3d3","lightgreen":"#90ee90","lightpink":"#ffb6c1","lightsalmon":"#ffa07a","lightseagreen":"#20b2aa","lightskyblue":"#87cefa","lightslategray":"#778899","lightsteelblue":"#b0c4de",
  "lightyellow":"#ffffe0","lime":"#00ff00","limegreen":"#32cd32","linen":"#faf0e6",
  "magenta":"#ff00ff","maroon":"#800000","mediumaquamarine":"#66cdaa","mediumblue":"#0000cd","mediumorchid":"#ba55d3","mediumpurple":"#9370d8","mediumseagreen":"#3cb371","mediumslateblue":"#7b68ee",
  "mediumspringgreen":"#00fa9a","mediumturquoise":"#48d1cc","mediumvioletred":"#c71585","midnightblue":"#191970","mintcream":"#f5fffa","mistyrose":"#ffe4e1","moccasin":"#ffe4b5",
  "navajowhite":"#ffdead","navy":"#000080",
  "oldlace":"#fdf5e6","olive":"#808000","olivedrab":"#6b8e23","orange":"#ffa500","orangered":"#ff4500","orchid":"#da70d6",
  "palegoldenrod":"#eee8aa","palegreen":"#98fb98","paleturquoise":"#afeeee","palevioletred":"#d87093","papayawhip":"#ffefd5","peachpuff":"#ffdab9","peru":"#cd853f","pink":"#ffc0cb","plum":"#dda0dd","powderblue":"#b0e0e6","purple":"#800080",
  "red":"#ff0000","rosybrown":"#bc8f8f","royalblue":"#4169e1",
  "saddlebrown":"#8b4513","salmon":"#fa8072","sandybrown":"#f4a460","seagreen":"#2e8b57","seashell":"#fff5ee","sienna":"#a0522d","silver":"#c0c0c0","skyblue":"#87ceeb","slateblue":"#6a5acd","slategray":"#708090","snow":"#fffafa","springgreen":"#00ff7f","steelblue":"#4682b4",
  "tan":"#d2b48c","teal":"#008080","thistle":"#d8bfd8","tomato":"#ff6347","turquoise":"#40e0d0",
  "violet":"#ee82ee",
  "wheat":"#f5deb3","white":"#ffffff","whitesmoke":"#f5f5f5",
  "yellow":"#ffff00","yellowgreen":"#9acd32"};

  if (typeof colours[colour.toLowerCase()] != 'undefined')
      return colours[colour.toLowerCase()];

  return false;
}

function HexToR(h) { return parseInt((cutHex(h)).substring(0,2),16) }
function HexToG(h) { return parseInt((cutHex(h)).substring(2,4),16) }
function HexToB(h) { return parseInt((cutHex(h)).substring(4,6),16) }
function cutHex(h) { return (h.charAt(0)=="#") ? h.substring(1,7) : h}
function RGBtoHex(R,G,B) {return toHex(R)+toHex(G)+toHex(B)}
function toHex(N) {
 if (N==null) return "00";
 N=parseInt(N); if (N==0 || isNaN(N)) return "00";
 N=Math.max(0,N); N=Math.min(N,255); N=Math.round(N);
 return "0123456789ABCDEF".charAt((N-N%16)/16)
      + "0123456789ABCDEF".charAt(N%16);
}

