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

"use strict";

var pgn4web_version = '3.08';

var pgn4web_project_url = "http://pgn4web.casaschi.net";
var pgn4web_project_author = "Paolo Casaschi";
var pgn4web_project_email = "pgn4web@casaschi.net";

var helpWin;

function displayHelp(section) {
    if (helpWin && !helpWin.closed) {
        helpWin.close();
    }
    helpWin = window.open(detectHelpLocation() + (section ? "?" + section : ""), "pgn4web_help", "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
    if (helpWin && window.focus) {
        helpWin.focus();
    }
}


// empty event APIs to be redefined

function customFunctionOnPgnTextLoad() {
}

function customFunctionOnPgnGameLoad() {
}

function customFunctionOnMove() {
}

function customFunctionOnAlert(msg) {
}

function customFunctionOnCheckLiveBroadcastStatus() {
}

// custom header tags APIs for customFunctionOnPgnGameLoad

function customPgnHeaderTag(customTag, htmlElementId, gameNum) {
    var matches, tag = "";
    customTag = customTag.replace(/\W+/g, "");
    if (gameNum === undefined) {
        gameNum = currentGame;
    }
    if ((pgnHeader[gameNum]) && (matches = pgnHeader[gameNum].match('\\[\\s*' + customTag + '\\s*\"([^\"]+)\"\\s*\\]'))) {
        tag = matches[1];
    }
    if (htmlElementId) {
        var theObj = document.getElementById(htmlElementId);
        if ((theObj) && (typeof (theObj.innerHTML) == "string")) {
            theObj.innerHTML = tag;
        }
    }
    return tag;
}

// custom comment tags API for customFunctionOnMove

function customPgnCommentTag(customTag, htmlElementId, plyNum, varId) {
    var matches, tag = "", theObj;
    customTag = customTag.replace(/\W+/g, "");
    if (typeof (varId) == "undefined") {
        varId = 0;
    }
    if (typeof (plyNum) == "undefined") {
        plyNum = CurrentPly;
    }
    if ((MoveCommentsVar[varId][plyNum]) && (matches = MoveCommentsVar[varId][plyNum].match('\\[%' + customTag + '\\s+((?:,?(?:"[^"]*"|[^,\\]]*))*)\\s*\\]'))) {
        tag = matches[1].replace(/\s+$/, "");
    }
    if ((htmlElementId) && (theObj = document.getElementById(htmlElementId)) && (typeof (theObj.innerHTML) == "string")) {
        theObj.innerHTML = tag;
    }
    return tag;
}


function simpleAddEvent(obj, evt, cbk) {
    if (obj.addEventListener) {
        obj.addEventListener(evt, cbk, false);
    } else if (obj.attachEvent) {
        obj.attachEvent("on" + evt, cbk);
    } // bugfix: IE8 and below specific
}

simpleAddEvent(document, "keydown", pgn4web_handleKey_event);
simpleAddEvent(window, "load", pgn4web_onload_event);


function pgn4web_onload_event(e) {
    pgn4web_onload(e);
}

function pgn4web_onload(e) {
    start_pgn4web();
}

function start_pgn4web() {
    // keep startup logs at the very first run, otherwise reset
    if (alertFirstResetLoadingPgn) {
        alertFirstResetLoadingPgn = false;
    } else {
        resetAlert();
    }
    InitImages();
    createBoard();
    pgn4web_initTouchEvents();
}

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(5);
    alertLast = alertLog.length - 1;
    alertNum = alertNumSinceReset = fatalErrorNumSinceReset = 0;
    stopAlertPrompt();
    if (!alertFirstResetLoadingPgn) {
        if (boardIsDefault(debugShortcutSquare)) {
            boardShortcut(debugShortcutSquare, "pgn4web v" + pgn4web_version + " debug info", null, true);
        }
    }
}

function myAlert(msg, fatalError, doNotPrompt) {
    alertNum++;
    alertNumSinceReset++;
    if (fatalError) {
        fatalErrorNumSinceReset++;
    }
    alertLast = (alertLast + 1) % alertLog.length;
    alertLog[alertLast] = msg + "\n" + (new Date()).toLocaleString();
    if (boardIsDefault(debugShortcutSquare)) {
        boardShortcut(debugShortcutSquare, "pgn4web v" + pgn4web_version + " debug info\n" + alertNum + " alert" + (alertNum > 1 ? "s" : ""), null, true);
    }
    if ((!doNotPrompt) && ((LiveBroadcastDelay === 0) || (LiveBroadcastAlert === true)) && (boardIsDefault(debugShortcutSquare))) {
        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;
    }
    var colRow = colRowFromSquare(debugShortcutSquare);
    if (!colRow) {
        return;
    }

    var alertPromptDelay = 1500; // for alerts before the board is printed
    var theObj = document.getElementById('tcol' + colRow.col + 'trow' + colRow.row);
    if (theObj) {
        if (alertPromptOn) {
            if ((highlightOption) && ((colFromHighlighted === 0 && rowFromHighlighted === 7) || (colToHighlighted === 0 && rowToHighlighted === 7))) {
                theObj.className = 'highlightWhiteSquare';
            } else {
                theObj.className = 'whiteSquare';
            }
        } else {
            theObj.className = 'blackSquare';
        }

        alertPromptOn = !alertPromptOn;
        if (alertPromptOn) {
            alertPromptDelay = 500;
        } else {
            alertPromptDelay = 3000;
        }
    }
    if (restart) {
        alertPromptInterval = setTimeout("alertPromptTick(true);", alertPromptDelay);
    }
}


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

// for onFocus/onBlur textbox events, 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() {
}

function pgn4web_handleKey_event(e) {
    pgn4web_handleKey(e);
}

var shortcutKeysEnabled = false;

function pgn4web_handleKey(e) {
    var keycode, oldPly, oldVar, colRow, colRowList;

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

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

    keycode = e.keyCode;

    // shift-escape always enabled: toggle shortcut keys
    if (!shortcutKeysEnabled && !(keycode == 27 && e.shiftKey)) {
        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 92: // super
        case 93: // menu
        case 188: // comma
            return true;

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

        case 189: // dash
            if (colRowList = prompt("Enter shortcut square coordinates to click:", "")) {
                colRowList = colRowList.toUpperCase().replace(/[^A-Z0-9]/g, "");
                while (colRow = colRowFromSquare(colRowList)) {
                    boardOnClick[colRow.col][colRow.row]({"id": "img_tcol" + colRow.col + "trow" + colRow.row}, e);
                    colRowList = colRowList.substr(2);
                }
            }
            break;

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

        case 37: // left-arrow
        case 74: // j
            backButton(e);
            break;

        case 38: // up-arrow
        case 72: // h
            startButton(e);
            break;

        case 39: // right-arrow
        case 75: // k
            forwardButton(e);
            break;

        case 40: // down-arrow
        case 76: // l
            endButton(e);
            break;

        case 73: // i
            MoveToPrevComment(e.shiftKey);
            break;

        case 79: // o
            MoveToNextComment(e.shiftKey);
            break;

        case 190: // dot
            if (e.shiftKey) {
                goToFirstChild();
            } else {
                goToNextVariationSibling();
            }
            break;

        case 85: // u
            if (e.shiftKey) {
                undoStackRedo();
            } else {
                undoStackUndo();
            }
            break;

        case 45: // insert
            undoStackRedo();
            break;

        case 46: // delete
            undoStackUndo();
            break;

        case 83: // s
            if (e.shiftKey) {
                searchPgnGame("");
            } else {
                searchPgnGamePrompt();
            }
            break;

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

        case 68: // d
            if (e.shiftKey) {
                displayFenData();
            } else {
                displayPgnData(true);
            }
            break;

        case 187: // equal
            SwitchAutoPlay();
            break;

        case 65: // a
            GoToMove(CurrentPly + 1);
            SetAutoPlay(true);
            break;

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

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

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

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

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

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

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

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

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

        case 57: // 9
            if (e.shiftKey) {
                customShortcutKey_Shift_9();
            } else {
                setCustomAutoplayDelay();
            }
            break;

        case 81: // q
            SetAutoplayDelayAndStart(10 * 1000);
            break;

        case 87: // w
            SetAutoplayDelayAndStart(20 * 1000);
            break;

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

        case 82: // r
            pauseLiveBroadcast();
            break;

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

        case 89: // y
            restartLiveBroadcast();
            break;

        case 70: // f
            if (!e.shiftKey || IsRotated) {
                FlipBoard();
            }
            break;

        case 71: // g
            SetHighlight(!highlightOption);
            break;

        case 88: // x
            randomGameRandomPly();
            break;

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

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

        case 66: // b
            Init(currentGame - 1);
            break;

        case 78: // n
            Init(currentGame + 1);
            break;

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

        case 80: // p
            if (e.shiftKey) {
                SetCommentsOnSeparateLines(!commentsOnSeparateLines);
            } else {
                SetCommentsIntoMoveText(!commentsIntoMoveText);
            }
            oldPly = CurrentPly;
            oldVar = CurrentVar;
            Init();
            GoToMove(oldPly, oldVar);
            break;

        default:
            return true;
    }
    return stopEvProp(e);
}

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

function colRowFromSquare(square) {
    if ((typeof (square) != "string") || (!square)) {
        return null;
    }
    var col = square.charCodeAt(0) - 65; // 65="A"
    if ((col < 0) || (col > 7)) {
        return null;
    }
    var row = 56 - square.charCodeAt(1); // 56="8"
    if ((row < 0) || (row > 7)) {
        return null;
    }
    return {"col": col, "row": row};
}

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

function boardIsDefault(square) {
    var colRow = colRowFromSquare(square);
    if (!colRow) {
        return false;
    }
    return boardDefault[colRow.col][colRow.row];
}

function boardShortcut(square, title, functionPointer, defaultSetting) {
    var theObj, colRow = colRowFromSquare(square);
    if (!colRow) {
        return;
    } else {
        var col = colRow.col;
        var row = colRow.row;
    }
    boardTitle[col][row] = title;
    if (functionPointer) {
        boardOnClick[col][row] = functionPointer;
    }
    boardDefault[col][row] = defaultSetting ? true : false;
    if (theObj = document.getElementById('img_tcol' + col + 'trow' + row)) {
        if (IsRotated) {
            square = String.fromCharCode(72 - col, 49 + row);
        }
        theObj.title = square + (boardTitle[col][row] ? ': ' + boardTitle[col][row] : '');
    }
}

// boardShortcut() always assumes 'square' defined as with white on bottom

var debugShortcutSquare = "A8";


boardShortcut("A8", "pgn4web v" + pgn4web_version + " debug info", function (t, e) {
    displayDebugInfo();
}, true);

boardShortcut("B8", "show current position FEN string", function (t, e) {
    displayFenData();
}, true);

boardShortcut("C8", "show current game PGN source data", function (t, e) {
    if (e.shiftKey) {
        savePgnData(true);
    } else {
        displayPgnData(true);
    }
}, true);

boardShortcut("D8", "show all games PGN source data", function (t, e) {
    if (e.shiftKey) {
        savePgnData();
    } else {
        displayPgnData();
    }
}, true);

boardShortcut("E8", "search help", function (t, e) {
    displayHelp("search_tool");
}, true);

boardShortcut("F8", "shortcut keys help", function (t, e) {
    displayHelp("shortcut_keys");
}, true);

boardShortcut("G8", "shortcut squares help", function (t, e) {
    displayHelp(e.shiftKey ? "informant_symbols" : "shortcut_squares");
}, true);

boardShortcut("H8", "pgn4web help", function (t, e) {
    displayHelp(e.shiftKey ? "credits_and_license" : "");
}, true);

boardShortcut("A7", "pgn4web website", function (t, e) {
    window.open(pgn4web_project_url);
}, true);

boardShortcut("B7", "undo last chessboard position update", function (t, e) {
    undoStackUndo();
}, true);

boardShortcut("C7", "redo last undo", function (t, e) {
    undoStackRedo();
}, true);

boardShortcut("D7", "toggle highlight last move", function (t, e) {
    SetHighlight(!highlightOption);
}, true);

boardShortcut("E7", "flip board", function (t, e) {
    if (!e.shiftKey || IsRotated) {
        FlipBoard();
    }
}, true);

boardShortcut("F7", "toggle show comments in game text", function (t, e) {
    if (e.shiftKey) {
        SetCommentsOnSeparateLines(!commentsOnSeparateLines);
    } else {
        SetCommentsIntoMoveText(!commentsIntoMoveText);
    }
    var oldPly = CurrentPly;
    var oldVar = CurrentVar;
    Init();
    GoToMove(oldPly, oldVar);
}, true);

boardShortcut("G7", "", function (t, e) {
}, true); // see setG7A6B6H6...

boardShortcut("H7", "toggle enabling shortcut keys", function (t, e) {
    interactivelyToggleShortcutKeys();
}, true);

boardShortcut("A6", "", function (t, e) {
}, true); // see setG7A6B6H6...

boardShortcut("B6", "", function (t, e) {
}, true); // see setG7A6B6H6...

boardShortcut("C6", "search previous finished game", function (t, e) {
    searchPgnGame('\\[\\s*Result\\s*"(?!\\*"\\s*\\])', true);
});

boardShortcut("D6", "search previous unfinished game", function (t, e) {
    searchPgnGame('\\[\\s*Result\\s*"\\*"\\s*\\]', true);
});

boardShortcut("E6", "search next unfinished game", function (t, e) {
    searchPgnGame('\\[\\s*Result\\s*"\\*"\\s*\\]', false);
}, true);

boardShortcut("F6", "search next finished game", function (t, e) {
    searchPgnGame('\\[\\s*Result\\s*"(?!\\*"\\s*\\])', false);
}, true);

boardShortcut("G6", "", function (t, e) {
}, true);

boardShortcut("H6", "", function (t, e) {
}, true); // see setG7A6B6H6...

boardShortcut("A5", "repeat last search backward", function (t, e) {
    searchPgnGame(lastSearchPgnExpression, true);
}, true);

boardShortcut("B5", "search prompt", function (t, e) {
    if (e.shiftKey) {
        searchPgnGame("");
    } else {
        searchPgnGamePrompt();
    }
}, true);

boardShortcut("C5", "repeat last search", function (t, e) {
    searchPgnGame(lastSearchPgnExpression);
}, true);

boardShortcut("D5", "search previous win result", function (t, e) {
    searchPgnGame('\\[\\s*Result\\s*"(1-0|0-1)"\\s*\\]', true);
}, true);

boardShortcut("E5", "search next win result", function (t, e) {
    searchPgnGame('\\[\\s*Result\\s*"(1-0|0-1)"\\s*\\]', false);
}, true);

boardShortcut("F5", "", function (t, e) {
}, true);

boardShortcut("G5", "", function (t, e) {
}, true);

boardShortcut("H5", "", function (t, e) {
}, true);

boardShortcut("A4", "search previous event", function (t, e) {
    searchPgnGame('\\[\\s*Event\\s*"(?!' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\])', true);
}, true);

boardShortcut("B4", "search previous round of same event", function (t, e) {
    searchPgnGame('\\[\\s*Event\\s*"' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\].*\\[\\s*Round\\s*"(?!' + fixRegExp(gameRound[currentGame]) + '"\\s*\\])|\\[\\s*Round\\s*"(?!' + fixRegExp(gameRound[currentGame]) + '"\\s*\\]).*\\[\\s*Event\\s*"' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\]', true);
}, true);

boardShortcut("C4", "search previous game of same black player", function (t, e) {
    searchPgnGame('\\[\\s*' + (e.shiftKey ? 'White' : 'Black') + '\\s*"' + fixRegExp(gameBlack[currentGame]) + '"\\s*\\]', true);
}, true);

boardShortcut("D4", "search previous game of same white player", function (t, e) {
    searchPgnGame('\\[\\s*' + (e.shiftKey ? 'Black' : 'White') + '\\s*"' + fixRegExp(gameWhite[currentGame]) + '"\\s*\\]', true);
}, true);

boardShortcut("E4", "search next game of same white player", function (t, e) {
    searchPgnGame('\\[\\s*' + (e.shiftKey ? 'Black' : 'White') + '\\s*"' + fixRegExp(gameWhite[currentGame]) + '"\\s*\\]', false);
}, true);

boardShortcut("F4", "search next game of same black player", function (t, e) {
    searchPgnGame('\\[\\s*' + (e.shiftKey ? 'White' : 'Black') + '\\s*"' + fixRegExp(gameBlack[currentGame]) + '"\\s*\\]', false);
}, true);

boardShortcut("G4", "search next round of same event", function (t, e) {
    searchPgnGame('\\[\\s*Event\\s*"' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\].*\\[\\s*Round\\s*"(?!' + fixRegExp(gameRound[currentGame]) + '"\\s*\\])|\\[\\s*Round\\s*"(?!' + fixRegExp(gameRound[currentGame]) + '"\\s*\\]).*\\[\\s*Event\\s*"' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\]', false);
}, true);

boardShortcut("H4", "search next event", function (t, e) {
    searchPgnGame('\\[\\s*Event\\s*"(?!' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\])', false);
}, true);

boardShortcut("A3", "load first game", function (t, e) {
    if (numberOfGames > 1) {
        Init(0);
    }
}, true);

boardShortcut("B3", "jump to previous games decile", function (t, e) {
    if (currentGame > 0) {
        calculateDeciles();
        for (var ii = (deciles.length - 2); ii >= 0; ii--) {
            if (currentGame > deciles[ii]) {
                Init(deciles[ii]);
                break;
            }
        }
    }
}, true);

boardShortcut("C3", "load previous game", function (t, e) {
    Init(currentGame - 1);
}, true);

boardShortcut("D3", "load random game", function (t, e) {
    if (numberOfGames > 1) {
        Init(Math.floor(Math.random() * numberOfGames));
    }
}, true);

boardShortcut("E3", "load random game at random position", function (t, e) {
    randomGameRandomPly();
}, true);

boardShortcut("F3", "load next game", function (t, e) {
    Init(currentGame + 1);
}, true);

boardShortcut("G3", "jump to next games decile", function (t, e) {
    if (currentGame < numberOfGames - 1) {
        calculateDeciles();
        for (var ii = 1; ii < deciles.length; ii++) {
            if (currentGame < deciles[ii]) {
                Init(deciles[ii]);
                break;
            }
        }
    }
}, true);

boardShortcut("H3", "load last game", function (t, e) {
    if (numberOfGames > 1) {
        Init(numberOfGames - 1);
    }
}, true);

boardShortcut("A2", "stop autoplay", function (t, e) {
    SetAutoPlay(e.shiftKey);
}, true);

boardShortcut("B2", "toggle autoplay", function (t, e) {
    SwitchAutoPlay();
}, true);

boardShortcut("C2", "autoplay 1 second", function (t, e) {
    SetAutoplayDelayAndStart((e.shiftKey ? 10 : 1) * 1000);
}, true);

boardShortcut("D2", "autoplay 2 seconds", function (t, e) {
    SetAutoplayDelayAndStart((e.shiftKey ? 20 : 2) * 1000);
}, true);

boardShortcut("E2", "autoplay 5 seconds", function (t, e) {
    SetAutoplayDelayAndStart((e.shiftKey ? 50 : 5) * 1000);
}, true);

boardShortcut("F2", "autoplay custom delay", function (t, e) {
    setCustomAutoplayDelay();
}, true);

boardShortcut("G2", "replay up to 6 previous half-moves, then autoplay forward", function (t, e) {
    replayPreviousMoves(e.shiftKey ? 10 : 6);
}, true);

boardShortcut("H2", "replay the previous half-move, then autoplay forward", function (t, e) {
    replayPreviousMoves(e.shiftKey ? 3 : 1);
}, true);

boardShortcut("A1", "go to game start", function (t, e) {
    startButton(e);
}, true);

boardShortcut("B1", "", function (t, e) {
}, true); // see setB1C1F1G1...

boardShortcut("C1", "", function (t, e) {
}, true); // see setB1C1F1G1...

boardShortcut("D1", "move backward", function (t, e) {
    backButton(e);
}, true);

boardShortcut("E1", "move forward", function (t, e) {
    forwardButton(e);
}, true);

boardShortcut("F1", "", function (t, e) {
}, true); // see setB1C1F1G1...

boardShortcut("G1", "", function (t, e) {
}, true); // see setB1C1F1G1...

boardShortcut("H1", "go to game end", function (t, e) {
    endButton(e);
}, true);


setG7A6B6H7boardShortcuts();

function setG7A6B6H7boardShortcuts() {
    if (LiveBroadcastDelay > 0) {
        if (boardIsDefault("G7")) {
            boardShortcut("G7", "", function (t, e) {
            }, true);
        }
        if (boardIsDefault("A6")) {
            boardShortcut("A6", "pause live broadcast automatic games refresh", function (t, e) {
                pauseLiveBroadcast();
            }, true);
        }
        if (boardIsDefault("B6")) {
            boardShortcut("B6", "restart live broadcast automatic games refresh", function (t, e) {
                restartLiveBroadcast();
            }, true);
        }
        if (boardIsDefault("H6")) {
            boardShortcut("H6", "force live broadcast games refresh", function (t, e) {
                refreshPgnSource();
            }, true);
        }
    } else {
        if (boardIsDefault("G7")) {
            boardShortcut("G7", "toggle autoplay next game", function (t, e) {
                SetAutoplayNextGame(!autoplayNextGame);
            }, true);
        }
        if (boardIsDefault("A6")) {
            boardShortcut("A6", "", function (t, e) {
            }, true);
        }
        if (boardIsDefault("B6")) {
            boardShortcut("B6", "", function (t, e) {
            }, true);
        }
        if (boardIsDefault("H6")) {
            boardShortcut("H6", "", function (t, e) {
            }, true);
        }
    }
}

setB1C1F1G1boardShortcuts();

function setB1C1F1G1boardShortcuts() {
    if (commentsIntoMoveText && GameHasComments) {
        if (boardIsDefault("B1")) {
            boardShortcut("B1", "find previous comment or variation", function (t, e) {
                if (e.shiftKey) {
                    GoToMove(CurrentPly - 10);
                } else {
                    MoveToPrevComment();
                }
            }, true);
        }
        if (boardIsDefault("G1")) {
            boardShortcut("G1", "find next comment or variation", function (t, e) {
                if (e.shiftKey) {
                    GoToMove(CurrentPly + 10);
                } else {
                    MoveToNextComment();
                }
            }, true);
        }
    } else {
        if (boardIsDefault("B1")) {
            boardShortcut("B1", "move 10 half-moves backward", function (t, e) {
                GoToMove(CurrentPly - 10);
            }, true);
        }
        if (boardIsDefault("G1")) {
            boardShortcut("G1", "move 10 half-moves forward", function (t, e) {
                GoToMove(CurrentPly + 10);
            }, true);
        }
    }
    if (commentsIntoMoveText && GameHasVariations) {
        if (boardIsDefault("C1")) {
            boardShortcut("C1", "go to parent variation", function (t, e) {
                if (e.shiftKey) {
                    GoToMove(CurrentPly - 6);
                } else {
                    GoToMove(StartPlyVar[CurrentVar]);
                }
            }, true);
        }
        if (boardIsDefault("F1")) {
            boardShortcut("F1", "cycle through alternative variations, if any, otherwise move forward", function (t, e) {
                if (e.shiftKey) {
                    GoToMove(CurrentPly + 6);
                } else {
                    if (!goToNextVariationSibling()) {
                        GoToMove(CurrentPly + 1);
                    }
                }
            }, true);
        }
    } else {
        if (boardIsDefault("C1")) {
            boardShortcut("C1", "move 6 half-moves backward", function (t, e) {
                GoToMove(CurrentPly - 6);
            }, true);
        }
        if (boardIsDefault("F1")) {
            boardShortcut("F1", "move 6 half-moves forward", function (t, e) {
                GoToMove(CurrentPly + 6);
            }, true);
        }
    }
}


var deciles = new Array(11);

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

function replayPreviousMoves(numPlies) {
    var thisPly = numPlies ? CurrentPly - numPlies : StartPly;
    if (thisPly < StartPlyVar[CurrentVar]) {
        thisPly = StartPlyVar[CurrentVar] + (CurrentVar === 0 ? 0 : 1);
    }
    if (thisPly !== CurrentPly) {
        GoToMove(thisPly);
    }
    SetAutoPlay(true);
}

function detectJavascriptLocation(jsre) {
    if (typeof (jsre) == "undefined") {
        jsre = new RegExp("pgn4web(|-compacted|\.min)\.js$", "");
    }
    var e = document.getElementsByTagName("script");
    for (var i = 0; i < e.length; i++) {
        if ((e[i].src) && (e[i].src.match(jsre))) {
            return e[i].src;
        }
    }
    return "";
}

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

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


var debugWin;

function displayDebugInfo() {
    var theObj;
    var bu = detectBaseLocation();
    var ju = detectJavascriptLocation();
    stopAlertPrompt();
    var dbg1 = 'pgn4web: version=' + pgn4web_version + ' homepage=' + pgn4web_project_url + '\n\nHTMLURL: length=' + location.href.length + ' url=';
    var dbg2 = location.href.length < 100 ? location.href : (location.href.substring(0, 99) + '...');
    var dbg3 = (bu ? '\nBASEURL: url=' + bu : '') + (ju != 'pgn4web.js' ? '\nJSURL: url=' + ju : '');
    if (pgnUrl) {
        dbg3 += '\nPGNURL: url=' + pgnUrl;
    } else if (theObj = document.getElementById("pgnText")) {
        dbg3 += '\nPGNTEXT: length=' + (theObj.tagName.toLowerCase() == 'textarea' ? theObj.value.length : '?');
    }
    dbg3 += '\n' + (numberOfGames > 1 ? '\nGAME: current=' + (currentGame + 1) + ' number=' + numberOfGames : '') + (numberOfVars > 1 ? '\nVARIATION: current=' + CurrentVar + ' number=' + (numberOfVars - 1) : '') + (PlyNumber > 0 ? '\nPLY: start=' + StartPly + ' current=' + CurrentPly + ' number=' + PlyNumber : '') + '\nAUTOPLAY: status=' + (isAutoPlayOn ? 'on' : 'off') + ' delay=' + Delay + 'ms' + ' next=' + autoplayNextGame;
    if ((typeof (gameVariant[currentGame]) !== "undefined") && (gameVariant[currentGame].match(/^\s*(|chess|normal|standard)\s*$/i) === null)) {
        dbg3 += '\nVARIANT: ' + gameVariant[currentGame];
    }
    if (LiveBroadcastDelay > 0) {
        dbg3 += '\n\nLIVEBROADCAST: status=' + liveStatusDebug() + (LiveBroadcastEndlessMode ? '/endless ' : '') + ' ticker=' + LiveBroadcastTicker + ' delay=' + LiveBroadcastDelay + 'm' + '\n' + 'refreshed: ' + LiveBroadcastLastRefreshedLocal + '\n' + 'received: ' + LiveBroadcastLastReceivedLocal + '\n' + 'modified (server time): ' + LiveBroadcastLastModified_ServerTime();
    }
    if (typeof (engineWinCheck) == "function") {
        dbg3 += '\n\nANALYSIS: ' + (engineWinCheck() ? 'board=connected ' + engineWin.customDebugInfo() : 'board=disconnected');
    }
    var thisInfo = customDebugInfo();
    if (thisInfo) {
        dbg3 += '\n\nCUSTOM: ' + thisInfo;
    }
    dbg3 += '\n\nALERTLOG: fatalnew=' + fatalErrorNumSinceReset + ' new=' + alertNumSinceReset + ' shown=' + Math.min(alertNum, alertLog.length) + ' total=' + alertNum + '\n--';
    if (alertNum > 0) {
        for (var ii = 0; ii < alertLog.length; ii++) {
            if (alertLog[(alertNum - 1 - ii) % alertLog.length] === undefined) {
                break;
            } else {
                dbg3 += "\n" + alertLog[(alertNum - 1 - ii) % alertLog.length] + "\n--";
            }
        }
    }
    if (confirm(dbg1 + dbg2 + dbg3 + '\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("", "pgn4web_debug_data", "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
        if (debugWin) {
            debugWin.document.open("text/html", "replace");
            debugWin.document.write("<html><head><title>pgn4web debug info</title><link rel='icon' sizes='16x16' href='pawn.ico' /></head><body>\n<pre>\n" + dbg1 + location.href + " " + dbg3 + "\n</pre>\n</body></html>");
            debugWin.document.close();
            if (window.focus) {
                debugWin.focus();
            }
        }
    }
    alertNumSinceReset = fatalErrorNumSinceReset = 0;
}

function liveStatusDebug() {
    if (LiveBroadcastEnded) {
        return "ended";
    }
    if (LiveBroadcastPaused) {
        return "paused";
    }
    if (LiveBroadcastStarted) {
        return "started";
    }
    return "waiting";
}

function customDebugInfo() {
    return "";
}

var pgnWin;

function displayPgnData(oneGameOnly) {
    if (pgnWin && !pgnWin.closed) {
        pgnWin.close();
    }
    pgnWin = window.open("", "pgn4web_pgn_data", "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
    if (pgnWin) {
        var text = "<html><head><title>pgn4web PGN source</title><link rel='icon' sizes='16x16' href='pawn.ico' /></head><body>\n<pre>\n";
        if (oneGameOnly) {
            text += fullPgnGame(currentGame) + "\n\n";
        } else {
            for (var ii = 0; ii < numberOfGames; ++ii) {
                text += fullPgnGame(ii) + "\n\n";
            }
        }
        text += "\n</pre>\n</body></html>";
        pgnWin.document.open("text/html", "replace");
        pgnWin.document.write(text);
        pgnWin.document.close();
        if (window.focus) {
            pgnWin.focus();
        }
    }
}

function savePgnData(oneGameOnly) {
    if (pgnUrl && !oneGameOnly) {
        location.href = pgnUrl;
    } else {
        displayPgnData(oneGameOnly); // fallback on displayPgnData for now
    }
}

function CurrentFEN() {
    var thisFEN = "";

    var emptySquares = 0;
    for (var row = 7; row >= 0; row--) {
        for (var col = 0; col <= 7; col++) {
            if (Board[col][row] === 0) {
                emptySquares++;
            } else {
                if (emptySquares) {
                    thisFEN += emptySquares;
                    emptySquares = 0;
                }
                if (Board[col][row] > 0) {
                    thisFEN += PiecesArr[Board[col][row] - 1].toUpperCase();
                } else if (Board[col][row] < 0) {
                    thisFEN += PiecesArr[-Board[col][row] - 1].toLowerCase();
                }
            }
        }
        if (emptySquares) {
            thisFEN += emptySquares;
            emptySquares = 0;
        }
        if (row > 0) {
            thisFEN += "/";
        }
    }

    thisFEN += CurrentPly % 2 ? " b" : " w";

    // castling availability: always in the KQkq form
    // knownbug: wrong FEN for Chess960 positions with inner castling rook
    var CastlingFEN = "";
    if (RookForOOCastling(0) !== null) {
        CastlingFEN += PiecesArr[0].toUpperCase();
    }
    if (RookForOOOCastling(0) !== null) {
        CastlingFEN += PiecesArr[1].toUpperCase();
    }
    if (RookForOOCastling(1) !== null) {
        CastlingFEN += PiecesArr[0].toLowerCase();
    }
    if (RookForOOOCastling(1) !== null) {
        CastlingFEN += PiecesArr[1].toLowerCase();
    }
    thisFEN += " " + (CastlingFEN || "-");

    if (HistEnPassant[CurrentPly]) {
        thisFEN += " " + String.fromCharCode(HistEnPassantCol[CurrentPly] + 97);
        thisFEN += CurrentPly % 2 ? "3" : "6";
    } else {
        thisFEN += " -";
    }

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

    thisFEN += " " + (Math.floor(CurrentPly / 2) + 1);

    return thisFEN;
}

var fenWin;

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

    var thisFEN = CurrentFEN();

    var movesStr = "";
    var lineStart = 0;
    if (addGametext) {
        for (var thisPly = CurrentPly; thisPly <= StartPly + PlyNumber; thisPly++) {
            var addStr = "";
            if (thisPly == StartPly + PlyNumber) {
                addStr = (CurrentVar ? "*" : gameResult[currentGame] || "*");
            } else {
                if (thisPly % 2 === 0) {
                    addStr = (Math.floor(thisPly / 2) + 1) + ". ";
                } else if (thisPly == CurrentPly) {
                    addStr = (Math.floor(thisPly / 2) + 1) + "... ";
                }
                addStr += Moves[thisPly];
            }
            if (movesStr.length + addStr.length + 1 > lineStart + 80) {
                lineStart = movesStr.length;
                movesStr += "\n" + addStr;
            } else {
                if (movesStr.length > 0) {
                    movesStr += " ";
                }
                movesStr += addStr;
            }
        }
    }

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


var pgnHeader = new Array();
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 highlightedMoveId = "";

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

var initialGame = 1;
var initialVariation = 0;
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 LiveBroadcastGamesRunning = 0;
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 LiveBroadcastEndlessMode = false;

var ParseLastMoveError = 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;
var mvIsNull = 0;

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

// HistCol, HistRow: move history up to last replayed ply
// HistCol[0], HistRow[0]: "square from"; 0..7, 0..7 from A1
// HistCol[1], HistRow[1]: castling/capture
// HistCol[2], HistRow[2]: "square to"; 0..7, 0..7 from A1

var HistCol = new Array(3);
var HistRow = new Array(3);
var HistPieceId = new Array(2);
var HistType = new Array(2);
var HistVar = new Array();

var PieceCol = new Array(2);
var PieceRow = new Array(2);
var PieceType = new Array(2);
var 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);
    HistType[i] = new Array();
    HistPieceId[i] = new Array();
}

for (i = 0; i < 3; ++i) {
    HistCol[i] = new Array();
    HistRow[i] = new Array();
}

var HistEnPassant = new Array();
HistEnPassant[0] = false;
var HistEnPassantCol = new Array();
HistEnPassantCol[0] = -1;

var HistNull = new Array();
HistNull[0] = 0;

var PiecesArr = "KQRBNP".split("");
var FenStringStart = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
var columnsLetters = "ABCDEFGH";
var InitialHalfMoveClock = 0;

var PieceImg = new Array(new Array(6), new Array(6));
var ClearImg;

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

var highlightOption = true;

var commentsIntoMoveText = true;
var commentsOnSeparateLines = false;

var pgnUrl = '';

var CastlingLong = new Array(2);
var CastlingShort = new Array(2);
var Moves = new Array();
var MoveComments = new Array();

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 pgnHeaderBlockRegExp = /\s*(\[\s*\w+\s*"[^"]*"\s*\]\s*)+/;

var emptyPgnHeader = '[Event ""]\n[Site ""]\n[Date ""]\n[Round ""]\n[White ""]\n[Black ""]\n[Result ""]\n';
var alertPgn = emptyPgnHeader + "\n{error: click on the top left chessboard square for debug info}";

var pgn4webVariationRegExp = /\[%pgn4web_variation (\d+)\]/;
var pgn4webVariationRegExpGlobal = /\[%pgn4web_variation (\d+)\]/g;

var gameSelectorHead = ' &middot;&middot;&middot;';
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, thisCol;

    if (what == '--') {
        StoreMove(plyCount);
        return true;
    }

    // castling
    if (what == 'O-O') {
        if (!CheckLegalityOO()) {
            return false;
        }
        for (thisCol = PieceCol[MoveColor][0]; thisCol < 7; thisCol++) {
            if (IsCheck(thisCol, MoveColor * 7, MoveColor)) {
                return false;
            }
        }
        StoreMove(plyCount);
        return true;
    } else if (what == 'O-O-O') {
        if (!CheckLegalityOOO()) {
            return false;
        }
        for (thisCol = PieceCol[MoveColor][0]; thisCol > 1; thisCol--) {
            if (IsCheck(thisCol, MoveColor * 7, MoveColor)) {
                return false;
            }
        }
        StoreMove(plyCount);
        return true;
    }

    // capture: "square to" occupied by opposite color piece (except en-passant)
    if (!mvCapture) {
        if (Board[mvToCol][mvToRow] !== 0) {
            return false;
        }
    }
    if ((mvCapture) && (Color(Board[mvToCol][mvToRow]) != 1 - MoveColor)) {
        if ((mvPiece != 6) || (!HistEnPassant[plyCount]) || (HistEnPassantCol[plyCount] != mvToCol) || (mvToRow != 5 - 3 * MoveColor)) {
            return false;
        }
    }

    // promotion: "square to" moved piece different from piece
    if (mvIsPromotion) {
        if (mvPiece != 6) {
            return false;
        }
        if (mvPieceOnTo >= 6) {
            return false;
        }
        if (mvToRow != 7 * (1 - MoveColor)) {
            return false;
        }
    }

    // piece move: which same type piece could move there?
    var howManyCandidates = 0;
    var candidatePieceId = -1;
    for (var pieceId = 15; pieceId >= 0; --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) {
                // board updated: king in check?
                mvPieceId = pieceId;
                StoreMove(plyCount);
                if (!IsCheck(PieceCol[MoveColor][0], PieceRow[MoveColor][0], MoveColor)) {
                    howManyCandidates++;
                    candidatePieceId = pieceId;
                }
                UndoMove(plyCount);
            }
        }
    }

// patch: for rejecting rather than guessing ambiguous move replace the code till the end of this function with the following line
// if (howManyCandidates == 1) { mvPieceId = candidatePieceId; StoreMove(plyCount); return true; } else { return false; }

    if (howManyCandidates > 0) {
        mvPieceId = candidatePieceId;
        StoreMove(plyCount);
        if (howManyCandidates > 1) {
            var text = (Math.floor(plyCount / 2) + 1) + ((plyCount % 2) === 0 ? '. ' : '... ');
            myAlert('error: guessed ambiguous ply ' + text + MovesVar[CurrentVar][plyCount] + ' in game ' + (currentGame + 1) + ' variation ' + CurrentVar);
        }
        return true;
    }
    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;
    }
    if (!CheckClearWay(thisQueen)) {
        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;
    }
    if (!CheckClearWay(thisRook)) {
        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;
    }
    if (!CheckClearWay(thisBishop)) {
        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 RookForOOCastling(color) {
    if (CastlingShort[color] < 0) {
        return null;
    }
    if (PieceMoveCounter[color][0] > 0) {
        return null;
    }

    var legal = false;
    for (var thisRook = 0; thisRook < 16; thisRook++) {
        if ((PieceCol[color][thisRook] == CastlingShort[color]) && (PieceCol[color][thisRook] > PieceCol[color][0]) && (PieceRow[color][thisRook] == color * 7) && (PieceType[color][thisRook] == 3)) {
            legal = true;
            break;
        }
    }
    if (!legal) {
        return null;
    }
    if (PieceMoveCounter[color][thisRook] > 0) {
        return null;
    }

    return thisRook;
}

function CheckLegalityOO() {

    var thisRook = RookForOOCastling(MoveColor);
    if (thisRook === null) {
        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 RookForOOOCastling(color) {
    if (CastlingLong[color] < 0) {
        return null;
    }
    if (PieceMoveCounter[color][0] > 0) {
        return null;
    }

    var legal = false;
    for (var thisRook = 0; thisRook < 16; thisRook++) {
        if ((PieceCol[color][thisRook] == CastlingLong[color]) && (PieceCol[color][thisRook] < PieceCol[color][0]) && (PieceRow[color][thisRook] == color * 7) && (PieceType[color][thisRook] == 3)) {
            legal = true;
            break;
        }
    }
    if (!legal) {
        return null;
    }
    if (PieceMoveCounter[color][thisRook] > 0) {
        return null;
    }

    return thisRook;
}

function CheckLegalityOOO() {

    var thisRook = RookForOOOCastling(MoveColor);
    if (thisRook === null) {
        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 CleanMove(move) {
    move = move.replace(/[^a-wyzA-WYZ0-9#-]*/g, ''); // patch: pgn notation: remove/add '+' 'x' '=' chars for full chess informant style or pgn style for the game text
    if (move.match(/^[Oo0]/)) {
        move = move.replace(/[o0]/g, 'O').replace(/O(?=O)/g, 'O-');
    }
    move = move.replace(/ep/i, '');
    return move;
}

function GoToMove(thisPly, thisVar) {
    SetAutoPlay(false);
    if (typeof (thisVar) == "undefined") {
        thisVar = CurrentVar;
    } else {
        if (thisVar < 0) {
            thisVar = 0;
        } else if (thisVar >= numberOfVars) {
            thisVar = numberOfVars - 1;
        }
    }
    if (thisPly < 0) {
        thisPly = 0;
    } else if (thisPly >= StartPlyVar[thisVar] + PlyNumberVar[thisVar]) {
        thisPly = StartPlyVar[thisVar] + PlyNumberVar[thisVar];
    }

    if (thisVar === CurrentVar) {
        var diff = thisPly - CurrentPly;
        if (diff > 0) {
            MoveForward(diff);
        } else {
            MoveBackward(-diff);
        }
    } else {
        var backStart = StartPly;
        loopCommonPredecessor:
            for (var ii = PredecessorsVars[CurrentVar].length - 1; ii >= 0; ii--) {
                for (var jj = PredecessorsVars[thisVar].length - 1; jj >= 0; jj--) {
                    if (PredecessorsVars[CurrentVar][ii] === PredecessorsVars[thisVar][jj]) {
                        backStart = Math.min(PredecessorsVars[CurrentVar][ii + 1] ? StartPlyVar[PredecessorsVars[CurrentVar][ii + 1]] : CurrentPly, PredecessorsVars[thisVar][jj + 1] ? StartPlyVar[PredecessorsVars[thisVar][jj + 1]] : thisPly);
                        break loopCommonPredecessor;
                    }
                }
            }
        MoveBackward(CurrentPly - backStart, true);
        MoveForward(thisPly - backStart, thisVar);
    }
}


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 SetInitialHalfmove(number_or_string, always) {
    alwaysInitialHalfmove = (always === true);
    initialHalfmove = typeof (number_or_string) == "undefined" ? 0 : number_or_string;
}

function SetInitialVariation(number) {
    initialVariation = isNaN(number = parseInt(number, 10)) ? 0 : number;
}

function SetInitialGame(number_or_string) {
    initialGame = typeof (number_or_string) == "undefined" ? 1 : number_or_string;
}

function randomGameRandomPly() {
    if (numberOfGames > 1) {
        var oldInitialHalfmove = initialHalfmove;
        var oldAlwaysInitialHalfmove = alwaysInitialHalfmove;
        SetInitialHalfmove("random", true);
        Init(Math.floor(Math.random() * numberOfGames));
        SetInitialHalfmove(oldInitialHalfmove, oldAlwaysInitialHalfmove);
    }
}


// clock detection as [%clk 01:02]

function clockFromComment(plyNum) {
    return customPgnCommentTag("clk", null, plyNum);
}

function clockFromHeader(whiteToMove) {
    var clockString = customPgnHeaderTag("Clock") + "";
    var matches = clockString.match("^" + (whiteToMove ? "W" : "B") + "/(.*)$");
    if (matches) {
        return matches[1];
    } else {
        return null;
    }
}

function HighlightLastMove() {
    var theObj, moveId, text, ii, clockString, clockRegExp, clockMatch;

    undoStackStore();

    // remove old move highlighting
    if (highlightedMoveId) {
        if (theObj = document.getElementById(highlightedMoveId)) {
            theObj.className = (highlightedMoveId.match(/Var0Mv/) ? 'move' : 'variation') + ' notranslate';
        }
    }

    // halfmove to be highlighted, negative for starting position
    var showThisMove = CurrentPly - 1;
    if (showThisMove > StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]) {
        showThisMove = StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar];
    }

    if (theObj = document.getElementById("GameLastComment")) {
        if (commentsIntoMoveText) {
            variationTextDepth = CurrentVar === 0 ? 0 : 1;
            text = '<SPAN CLASS="comment">' + strippedMoveComment(showThisMove + 1, CurrentVar, true).replace(/\sID="[^"]*"/g, '') + '</SPAN>';
        } else {
            text = '';
        }
        theObj.innerHTML = text;
    }

    // side to move
    var whiteToMove = ((showThisMove + 1) % 2 === 0);
    text = whiteToMove ? 'white' : 'black';

    if (theObj = document.getElementById("GameSideToMove")) {
        theObj.innerHTML = text;
    }

    // clock
    var lastMoverClockObj = document.getElementById(whiteToMove ? "GameBlackClock" : "GameWhiteClock");
    var initialLastMoverClock = whiteToMove ? gameInitialBlackClock[currentGame] : gameInitialWhiteClock[currentGame];
    var beforeLastMoverClockObj = document.getElementById(whiteToMove ? "GameWhiteClock" : "GameBlackClock");
    var initialBeforeLastMoverClock = whiteToMove ? gameInitialWhiteClock[currentGame] : gameInitialBlackClock[currentGame];

    if (lastMoverClockObj) {
        clockString = ((showThisMove + 1 === StartPly + PlyNumber) && ((!LiveBroadcastDemo) || (gameResult[currentGame] !== "*"))) ? clockFromHeader(!whiteToMove) : null;
        if (clockString === null) {
            clockString = showThisMove + 1 > StartPly ? clockFromComment(showThisMove + 1) : initialLastMoverClock;
            if (!clockString && (CurrentPly === StartPly + PlyNumber)) {
                // support for time info in the last comment as { White Time: 0h:12min Black Time: 1h:23min }
                clockRegExp = new RegExp((whiteToMove ? "Black" : "White") + "\\s+Time:\\s*(\\S+)", "i");
                if (clockMatch = strippedMoveComment(StartPly + PlyNumber).match(clockRegExp)) {
                    clockString = clockMatch[1];
                }
            }
        }
        lastMoverClockObj.innerHTML = clockString;
    }
    if (beforeLastMoverClockObj) {
        clockString = ((showThisMove + 1 === StartPly + PlyNumber) && ((!LiveBroadcastDemo) || (gameResult[currentGame] !== "*"))) ? clockFromHeader(whiteToMove) : null;
        if (clockString === null) {
            clockString = showThisMove > StartPly ? clockFromComment(showThisMove) : initialBeforeLastMoverClock;
            if (!clockString && (CurrentPly === StartPly + PlyNumber)) {
                // see comment above
                clockRegExp = new RegExp((whiteToMove ? "White" : "Black") + "\\s+Time:\\s*(\\S+)", "i");
                if (clockMatch = strippedMoveComment(StartPly + PlyNumber).match(clockRegExp)) {
                    clockString = clockMatch[1];
                }
            }
        }
        beforeLastMoverClockObj.innerHTML = clockString;
    }

    if (lastMoverClockObj && beforeLastMoverClockObj) {
        if (lastMoverClockObj.innerHTML && !beforeLastMoverClockObj.innerHTML) {
            beforeLastMoverClockObj.innerHTML = "-";
        } else if (!lastMoverClockObj.innerHTML && beforeLastMoverClockObj.innerHTML) {
            lastMoverClockObj.innerHTML = "-";
        }
    }

    // next move
    if (theObj = document.getElementById("GameNextMove")) {
        if (CurrentVar === 0 && showThisMove + 1 >= StartPly + PlyNumber) {
            text = '<SPAN CLASS="move notranslate">' + gameResult[currentGame] + '</SPAN>';
        } else if (typeof (Moves[showThisMove + 1]) == "undefined") {
            text = "";
        } else {
            text = printMoveText(showThisMove + 1, CurrentVar, (CurrentVar !== 0), true, false);
        }
        theObj.innerHTML = text;
    }

    // next variations
    if (theObj = document.getElementById("GameNextVariations")) {
        text = '';
        if (commentsIntoMoveText) {
            var children = childrenVars(showThisMove + 1, CurrentVar);
            for (ii = 0; ii < children.length; ii++) {
                if (children[ii] !== CurrentVar) {
                    text += ' ' + printMoveText(showThisMove + 1, children[ii], (children[ii] !== 0), true, false);
                }
            }
        }
        theObj.innerHTML = text;
    }

    // last move
    if (theObj = document.getElementById("GameLastMove")) {
        if ((showThisMove >= StartPly) && Moves[showThisMove]) {
            text = printMoveText(showThisMove, CurrentVar, (CurrentVar !== 0), true, false);
        } else if (showThisMove === StartPly - 1) {
            text = '<SPAN CLASS="move notranslate">' + (Math.floor((showThisMove + 1) / 2) + 1) + (((showThisMove + 1) % 2) ? "..." : ".") + '</SPAN>';
        } else {
            text = '';
        }
        theObj.innerHTML = text;
    }

    // last variations
    if (theObj = document.getElementById("GameLastVariations")) {
        text = '';
        if (commentsIntoMoveText) {
            var siblings = childrenVars(showThisMove, HistVar[showThisMove]);
            for (ii = 0; ii < siblings.length; ii++) {
                if (siblings[ii] !== CurrentVar) {
                    text += ' ' + printMoveText(showThisMove, siblings[ii], (siblings[ii] !== 0), true, false);
                }
            }
        }
        theObj.innerHTML = text;
    }

    if (showThisMove >= (StartPlyVar[CurrentVar] - 1)) {
        moveId = 'Var' + CurrentVar + 'Mv' + (showThisMove + 1);
        if (theObj = document.getElementById(moveId)) {
            theObj.className = (CurrentVar ? 'variation variationOn' : 'move moveOn') + ' notranslate';
        }
        highlightedMoveId = moveId;

        if (highlightOption) {
            var colFrom, rowFrom, colTo, rowTo;
            if ((showThisMove < StartPly) || HistNull[showThisMove]) {
                colFrom = rowFrom = -1;
                colTo = rowTo = -1;
            } else {
                colFrom = HistCol[0][showThisMove] === undefined ? -1 : HistCol[0][showThisMove];
                rowFrom = HistRow[0][showThisMove] === undefined ? -1 : HistRow[0][showThisMove];
                colTo = HistCol[2][showThisMove] === undefined ? -1 : HistCol[2][showThisMove];
                rowTo = HistRow[2][showThisMove] === undefined ? -1 : HistRow[2][showThisMove];
            }
            highlightMove(colFrom, rowFrom, colTo, rowTo);
        }
    }
}

function SetHighlightOption(on) {
    highlightOption = on;
}

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

var colFromHighlighted = -1;
var rowFromHighlighted = -1;
var colToHighlighted = -1;
var rowToHighlighted = -1;

function highlightMove(colFrom, rowFrom, colTo, rowTo) {
    highlightSquare(colFromHighlighted, rowFromHighlighted, false);
    highlightSquare(colToHighlighted, rowToHighlighted, false);
    if (highlightSquare(colFrom, rowFrom, true)) {
        colFromHighlighted = colFrom;
        rowFromHighlighted = rowFrom;
    } else {
        colFromHighlighted = rowFromHighlighted = -1;
    }
    if (highlightSquare(colTo, rowTo, true)) {
        colToHighlighted = colTo;
        rowToHighlighted = rowTo;
    } else {
        colToHighlighted = rowToHighlighted = -1;
    }
}

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

var undoStackMax = 1000;
var undoStackGame = new Array(undoStackMax);
var undoStackVar = new Array(undoStackMax);
var undoStackPly = new Array(undoStackMax);
var undoStackStart = 0;
var undoStackCurrent = 0;
var undoStackEnd = 0;
var undoRedoInProgress = false;

function undoStackReset() {
    undoStackGame = new Array(undoStackMax);
    undoStackVar = new Array(undoStackMax);
    undoStackPly = new Array(undoStackMax);
    undoStackStart = undoStackCurrent = undoStackEnd = 0;
}

function undoStackStore() {
    if (undoRedoInProgress) {
        return false;
    }
    if ((undoStackStart === undoStackCurrent) || (currentGame !== undoStackGame[undoStackCurrent]) || (CurrentVar !== undoStackVar[undoStackCurrent]) || (CurrentPly !== undoStackPly[undoStackCurrent])) {
        undoStackCurrent = (undoStackCurrent + 1) % undoStackMax;
        undoStackGame[undoStackCurrent] = currentGame;
        undoStackVar[undoStackCurrent] = CurrentVar;
        undoStackPly[undoStackCurrent] = CurrentPly;
        undoStackEnd = undoStackCurrent;
        if (undoStackStart === undoStackCurrent) {
            undoStackStart = (undoStackStart + 1) % undoStackMax;
        }
    }
    return true;
}

function undoStackUndo() {
    if ((undoStackCurrent - 1 + undoStackMax) % undoStackMax === undoStackStart) {
        return false;
    }
    undoRedoInProgress = true;
    undoStackCurrent = (undoStackCurrent - 1 + undoStackMax) % undoStackMax;
    if (undoStackGame[undoStackCurrent] !== currentGame) {
        Init(undoStackGame[undoStackCurrent]);
    }
    GoToMove(undoStackPly[undoStackCurrent], undoStackVar[undoStackCurrent]);
    undoRedoInProgress = false;
    return true;
}

function undoStackRedo() {
    if (undoStackCurrent === undoStackEnd) {
        return false;
    }
    undoRedoInProgress = true;
    undoStackCurrent = (undoStackCurrent + 1) % undoStackMax;
    if (undoStackGame[undoStackCurrent] !== currentGame) {
        Init(undoStackGame[undoStackCurrent]);
    }
    GoToMove(undoStackPly[undoStackCurrent], undoStackVar[undoStackCurrent]);
    undoRedoInProgress = false;
    return true;
}


function fixCommonPgnMistakes(text) {
    text = text.replace(/[\u00A0\u180E\u2000-\u200A\u202F\u205F\u3000]/g, " "); // some spaces 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 "..."
    text = text.replace(/\\"/g, "'"); // fix [Opening "Queen\"s gambit"]
    return text;
}

function fullPgnGame(gameNum) {
    var res = pgnHeader[gameNum] ? pgnHeader[gameNum].replace(/^[^[]*/g, "") : "";
    res = res.replace(/\[\s*(\w+)\s*"([^"]*)"\s*\][^[]*/g, '[$1 "$2"]\n');
    res += "\n";
    res += pgnGame[gameNum] ? pgnGame[gameNum].replace(/(^[\s]*|[\s]*$)/g, "") : "";
    return res;
}

function pgnGameFromPgnText(pgnText) {

    var newNumGames, headMatch, prevHead, newHead, startNew, afterNew, lastOpen, checkedGame, validHead;

    pgnText = simpleHtmlentities(fixCommonPgnMistakes(pgnText));

    // PGN standard: ignore lines starting with %
    pgnText = pgnText.replace(/(^|\n)%.*(\n|$)/g, "\n");

    newNumGames = 0;
    checkedGame = "";
    while (headMatch = pgnHeaderBlockRegExp.exec(pgnText)) {
        newHead = headMatch[0];
        startNew = pgnText.indexOf(newHead);
        afterNew = startNew + newHead.length;
        if (prevHead) {
            checkedGame += pgnText.slice(0, startNew);
            validHead = ((lastOpen = checkedGame.lastIndexOf("{")) < 0) || (checkedGame.lastIndexOf("}")) > lastOpen;
            if (validHead) {
                pgnHeader[newNumGames] = prevHead;
                pgnGame[newNumGames++] = checkedGame;
                checkedGame = "";
            } else {
                checkedGame += newHead;
            }
        } else {
            validHead = true;
        }
        if (validHead) {
            prevHead = newHead;
        }
        pgnText = pgnText.slice(afterNew);
    }
    if (prevHead) {
        pgnHeader[newNumGames] = prevHead;
        checkedGame += pgnText;
        pgnGame[newNumGames++] = checkedGame;
    }

    if (newNumGames === 0) {
        return false;
    }
    numberOfGames = newNumGames;
    return true;
}


function pgnGameFromHttpRequest(httpResponseData) {

    // process here any special file types, for instance zipfiles:
    //   if (pgnUrl && pgnUrl.replace(/[?#].*/, "").match(/\.zip$/i)) { return pgnGameFromPgnText(unzipPgnFiles(httpResponseData)); }
    // remember to fix function loadPgnFromPgnUrl() for binary data

    return pgnGameFromPgnText(httpResponseData);
}

var http_request_last_processed_id = 0;

function updatePgnFromHttpRequest(this_http_request, this_http_request_id) {
    var res = LOAD_PGN_FAIL;

    if (this_http_request.readyState != 4) {
        return;
    }

    if (this_http_request_id < http_request_last_processed_id) {
        return;
    } else {
        http_request_last_processed_id = this_http_request_id;
    }

    // patch: enable loading local PGN files on some browsers by adding: || (this_http_request.status === 0)
    if ((this_http_request.status == 200) || (this_http_request.status == 304)) {

        if (this_http_request.status == 304) {
            if (LiveBroadcastDelay > 0) {
                res = LOAD_PGN_UNMODIFIED;
            } else {
                myAlert('error: unmodified PGN URL when not in live mode');
            }
        } else if (!this_http_request.responseText) {
            myAlert('error: no data received from PGN URL\n' + pgnUrl, true);
        } else if (!pgnGameFromHttpRequest(this_http_request.responseText)) {
            myAlert('error: no games found at PGN URL\n' + pgnUrl, true);
        } else {
            if (LiveBroadcastDelay > 0) {
                LiveBroadcastLastReceivedLocal = (new Date()).toLocaleString();
                if (LiveBroadcastLastModifiedHeader = this_http_request.getResponseHeader("Last-Modified")) {
                    LiveBroadcastLastModified = new Date(LiveBroadcastLastModifiedHeader);
                } else {
                    LiveBroadcastLastModified_Reset();
                }
            }
            res = LOAD_PGN_OK;
        }

    } else {
        myAlert('error: failed reading PGN URL\n' + pgnUrl, true);
    }

    if (LiveBroadcastDemo && (res == LOAD_PGN_UNMODIFIED)) {
        res = LOAD_PGN_OK;
    }

    loadPgnCheckingLiveStatus(res);
}

var LOAD_PGN_FAIL = 0;
var LOAD_PGN_OK = 1;
var LOAD_PGN_UNMODIFIED = 2;

function loadPgnCheckingLiveStatus(res) {

    switch (res) {

        case LOAD_PGN_OK:
            if (LiveBroadcastDelay > 0) {
                firstStart = true;
                var oldParseLastMoveError = ParseLastMoveError;
                if (!LiveBroadcastStarted) {
                    LiveBroadcastStarted = true;
                } else {
                    var oldWhite = gameWhite[currentGame];
                    var oldBlack = gameBlack[currentGame];
                    var oldEvent = gameEvent[currentGame];
                    var oldRound = gameRound[currentGame];
                    var oldSite = gameSite[currentGame];
                    var oldDate = gameDate[currentGame];

                    initialGame = currentGame + 1;

                    LiveBroadcastOldCurrentVar = CurrentVar;
                    LiveBroadcastOldCurrentPly = CurrentPly;
                    LiveBroadcastOldCurrentPlyLast = (CurrentVar === 0 && CurrentPly === StartPlyVar[0] + PlyNumberVar[0]);

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

                    LoadGameHeaders();
                    LiveBroadcastFoundOldGame = false;
                    for (var ii = 0; ii < numberOfGames; ii++) {
                        LiveBroadcastFoundOldGame = (gameWhite[ii] == oldWhite) && (gameBlack[ii] == oldBlack) && (gameEvent[ii] == oldEvent) && (gameRound[ii] == oldRound) && (gameSite[ii] == oldSite); // && (gameDate[ii] ==oldDate );
                        if (LiveBroadcastFoundOldGame) {
                            break;
                        }
                    }
                    if (LiveBroadcastFoundOldGame) {
                        initialGame = ii + 1;
                    }

                    if (LiveBroadcastFoundOldGame) {
                        var oldInitialVariation = initialVariation;
                        var oldInitialHalfmove = initialHalfmove;
                        initialVariation = CurrentVar;
                        if (LiveBroadcastSteppingMode) {
                            initialHalfmove = (LiveBroadcastOldCurrentPlyLast || oldParseLastMoveError) ? LiveBroadcastOldCurrentPly + 1 : LiveBroadcastOldCurrentPly;
                        } else {
                            initialHalfmove = (LiveBroadcastOldCurrentPlyLast || oldParseLastMoveError) ? "end" : LiveBroadcastOldCurrentPly;
                        }
                    }
                }
            }

            undoStackReset();
            Init();

            if (LiveBroadcastDelay > 0) {
                if (LiveBroadcastFoundOldGame) {
                    initialHalfmove = oldInitialHalfmove;
                    initialVariation = oldInitialVariation;
                }
                checkLiveBroadcastStatus();
            }

            customFunctionOnPgnTextLoad();

            if (LiveBroadcastDelay > 0) {
                if (LiveBroadcastFoundOldGame) {
                    if (LiveBroadcastSteppingMode) {
                        if (oldAutoplay || LiveBroadcastOldCurrentPlyLast || oldParseLastMoveError) {
                            SetAutoPlay(true);
                        }
                    } else {
                        if (oldAutoplay) {
                            SetAutoPlay(true);
                        }
                    }
                }
            }

            break;

        case LOAD_PGN_UNMODIFIED:
            if (LiveBroadcastDelay > 0) {
                checkLiveBroadcastStatus();
            }
            break;

        case LOAD_PGN_FAIL:
        default:
            if (LiveBroadcastDelay === 0) {
                pgnGameFromPgnText(alertPgn);
                undoStackReset();
                Init();
                customFunctionOnPgnTextLoad();
            } else { // live broadcast: wait for live show start
                if (!LiveBroadcastStarted) {
                    pgnGameFromPgnText(LiveBroadcastPlaceholderPgn);
                    firstStart = true;
                    undoStackReset();
                    Init();
                    checkLiveBroadcastStatus();
                    customFunctionOnPgnTextLoad();
                } else {
                    checkLiveBroadcastStatus();
                }
            }
            break;

    }

    if (LiveBroadcastDelay > 0) {
        restartLiveBroadcastTimeout();
    }
}

var http_request_last_id = 0;

function loadPgnFromPgnUrl(pgnUrl) {

    LiveBroadcastLastRefreshedLocal = (new Date()).toLocaleString();

    try {
        var http_request = new XMLHttpRequest();
        if (http_request.overrideMimeType) {
            // patch: pgn encoding: amend the following line to match the expected encoding of the PGN file if charachters beyond the basic 128 ascii set are not displayed correctly
            http_request.overrideMimeType("text/plain"); // this assumes the PGN file is encoded as unicode UTF-8
            // http_request.overrideMimeType("text/plain; charset=ISO-8859-15"); // this has been reported to work with some files created by the DGT live boards software
            // http_request.overrideMimeType("text/plain; charset=x-user-defined"); // this works with a binary file, such as a zipfile, but would need further processing
        }
    } catch (e) {
        myAlert('error: failed creating XMLHttpRequest for PGN URL\n' + pgnUrl, true);
        loadPgnCheckingLiveStatus(LOAD_PGN_FAIL);
        return false;
    }

    var http_request_id = http_request_last_id++;
    http_request.onreadystatechange = function () {
        updatePgnFromHttpRequest(http_request, http_request_id);
    };

    try {
        var randomizer = "";
        // anti-caching #1
        if ((LiveBroadcastDelay > 0) && (pgnUrl.indexOf("?") == -1) && (pgnUrl.indexOf("#") == -1)) {
            randomizer = "?noCache=" + (0x1000000000 + Math.floor((Math.random() * 0xF000000000))).toString(16).toUpperCase();
        }
        http_request.open("GET", pgnUrl + randomizer);
        // anti-caching #2
        if (LiveBroadcastDelay > 0) {
            http_request.setRequestHeader("If-Modified-Since", LiveBroadcastLastModifiedHeader);
        }
        http_request.send(null);
    } catch (e) {
        myAlert('error: failed sending XMLHttpRequest for PGN URL\n' + pgnUrl, true);
        return false;
    }

    return true;
}

function SetPgnUrl(url) {
    pgnUrl = url;
}


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) || (LiveBroadcastPaused)) {
        return;
    }
    LiveBroadcastPaused = true;
    clearTimeout(LiveBroadcastInterval);
    LiveBroadcastInterval = null;
    LiveBroadcastTicker--;
    checkLiveBroadcastStatus();
    LiveBroadcastTicker++;
}

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

function checkLiveBroadcastStatus() {
    if (LiveBroadcastDelay === 0) {
        return;
    }

    var theTitle, theHTML, theObj, ii;

    // broadcast started yet?
    if (LiveBroadcastStarted === false || typeof (pgnHeader) == "undefined" || (numberOfGames == 1 && gameEvent[0] == LiveBroadcastPlaceholderEvent)) {
        // no
        LiveBroadcastEnded = false;
        LiveBroadcastGamesRunning = 0;
        theTitle = "live broadcast yet to start";
    } else {
        // yes
        var lbgr = 0, lbga = 0;
        for (ii = 0; ii < numberOfGames; ii++) {
            if (gameResult[ii].indexOf('*') >= 0) {
                lbga++;
                if (!pgnGame[ii].match(/^\s*\*?\s*$/)) {
                    lbgr++;
                }
            }
        }
        LiveBroadcastEnded = ((lbga === 0) && (!LiveBroadcastEndlessMode));
        LiveBroadcastGamesRunning = lbgr;
        theTitle = LiveBroadcastEnded ? "live broadcast ended" : LiveBroadcastPaused ? "live broadcast paused" : lbgr + " live game" + (lbgr == 1 ? "" : "s") + " out of " + numberOfGames;
    }
    theHTML = LiveBroadcastEnded ? "#" : LiveBroadcastPaused ? "+" : "=";
    theHTML = (LiveBroadcastTicker % 4 === 3 ? theHTML : "&nbsp;") + (LiveBroadcastTicker % 2 === 0 ? theHTML : "&nbsp;") + (LiveBroadcastTicker % 4 === 1 ? theHTML : "&nbsp;");
    theHTML = LiveBroadcastGamesRunning + "<span style='display:inline-block; min-width:3em; text-align:center;'>" + theHTML + "</span>" + numberOfGames;
    theHTML = "<span onclick='" + (LiveBroadcastPaused ? "restartLiveBroadcast();" : "refreshPgnSource();") + " this.blur();'>" + theHTML + "</span>";

    if (theObj = document.getElementById("GameLiveStatus")) {
        theObj.innerHTML = theHTML;
        theObj.title = theTitle;
    }

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

    customFunctionOnCheckLiveBroadcastStatus();
}

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

var LiveBroadcastFoundOldGame = false;
var LiveBroadcastOldCurrentVar;
var LiveBroadcastOldCurrentPly;
var LiveBroadcastOldCurrentPlyLast = false;

function refreshPgnSource() {
    if (LiveBroadcastDelay === 0) {
        return;
    }
    if (LiveBroadcastInterval) {
        clearTimeout(LiveBroadcastInterval);
        LiveBroadcastInterval = null;
    }
    if (LiveBroadcastDemo) {
        var newPly, addedPly = 0;
        for (var ii = 0; ii < numberOfGames; ii++) {
            //        5% 15%      40%
            newPly = [3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1][Math.floor(20 * Math.random())] || 0;
            if (gameDemoMaxPly[ii] <= gameDemoLength[ii]) {
                gameDemoMaxPly[ii] += newPly;
                addedPly += newPly;
            }
        }
        if (addedPly > 0) {
            LiveBroadcastLastReceivedLocal = (new Date()).toLocaleString();
        }
    }

    if (pgnUrl) {
        loadPgnFromPgnUrl(pgnUrl);
    } else if (document.getElementById("pgnText")) {
        loadPgnFromTextarea("pgnText");
    } else {
        pgnGameFromPgnText(alertPgn);
        undoStackReset();
        Init();
        customFunctionOnPgnTextLoad();
        myAlert('error: missing PGN URL location and pgnText object in the HTML file', true);
    }
}

function loadPgnFromTextarea(textareaId) {
    var res = LOAD_PGN_FAIL, text, theObj;

    LiveBroadcastLastRefreshedLocal = (new Date()).toLocaleString();

    if (!(theObj = document.getElementById(textareaId))) {
        myAlert('error: missing ' + textareaId + ' textarea object in the HTML file', true);
    } else {
        if (document.getElementById(textareaId).tagName.toLowerCase() == "textarea") {
            text = document.getElementById(textareaId).value;
        } else { // compatibility with pgn4web up to 1.77: <span> used for pgnText
            text = document.getElementById(textareaId).innerHTML;
            // fixes browser issue removing \n from innerHTML
            if (text.indexOf('\n') < 0) {
                text = text.replace(/((\[[^\[\]]*\]\s*)+)/g, "\n$1\n");
            }
            // fixes browser issue replacing quotes with &quot;
            if (text.indexOf('"') < 0) {
                text = text.replace(/(&quot;)/g, '"');
            }
        }

        // no header: add emptyPgnHeader
        if (pgnHeaderTagRegExp.test(text) === false) {
            text = emptyPgnHeader + "\n" + text;
        }

        if (pgnGameFromPgnText(text)) {
            res = LOAD_PGN_OK;
            LiveBroadcastLastReceivedLocal = (new Date()).toLocaleString();
        } else {
            myAlert('error: no games found in ' + textareaId + ' object in the HTML file');
        }
    }

    loadPgnCheckingLiveStatus(res);
}

function createBoard() {
    if (pgnUrl) {
        loadPgnFromPgnUrl(pgnUrl);
    } else if (document.getElementById("pgnText")) {
        loadPgnFromTextarea("pgnText");
    } else {
        pgnGameFromPgnText(alertPgn);
        undoStackReset();
        Init();
        customFunctionOnPgnTextLoad();
        myAlert('error: missing PGN URL location or pgnText in the HTML file', true);
    }
}

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() {
    var iv, ih;
    if (initialVariation < 0) {
        iv = Math.max(numberOfVars + initialVariations, 0);
    } else {
        iv = Math.min(initialVariation, numberOfVars - 1);
    }

    switch (initialHalfmove) {
        case "start":
            GoToMove((StartPlyVar[iv] + (iv ? 1 : 0)), iv);
            break;
        case "end":
            GoToMove(StartPlyVar[iv] + PlyNumberVar[iv], iv);
            break;
        case "random":
            GoToMove((StartPlyVar[iv] + (iv ? 1 : 0)) + Math.floor(Math.random() * (StartPlyVar[iv] + PlyNumberVar[iv])), iv);
            break;
        case "comment":
        case "variation":
            GoToMove((StartPlyVar[iv] + (iv ? 1 : 0)), iv);
            MoveToNextComment(initialHalfmove == "variation");
            break;
        default:
            if (isNaN(initialHalfmove = parseInt(initialHalfmove, 10))) {
                initialHalfmove = 0;
            }
            if (initialHalfmove < 0) {
                ih = Math.max(StartPlyVar[iv] + PlyNumberVar[iv] + 1 + initialHalfmove, StartPly);
            } else {
                ih = Math.min(initialHalfmove, StartPlyVar[iv] + PlyNumberVar[iv]);
            }
            GoToMove(ih, iv);
            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();
    }

    if ((gameSetUp[currentGame] !== undefined) && (gameSetUp[currentGame] != "1")) {
        InitFEN();
    } else {
        InitFEN(gameFEN[currentGame]);
    }

    OpenGame(currentGame);

    CurrentPly = StartPly;
    if (firstStart || alwaysInitialHalfmove) {
        GoToInitialHalfmove();
        setTimeout("autoScrollToCurrentMoveIfEnabled();", Math.min(666, 0.9 * Delay));
    } else {
        synchMoves();
        RefreshBoard();
        HighlightLastMove();
        autoScrollToCurrentMoveIfEnabled();
        // customFunctionOnMove here for consistency: null move starting new game
        customFunctionOnMove();
        if (typeof (engineWinOnMove) == "function") {
            engineWinOnMove();
        }
    }

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

    customFunctionOnPgnGameLoad();

    initialVariation = 0;
    firstStart = false;
}

function myAlertFEN(FenString, text) {
    myAlert("error: invalid FEN in game " + (currentGame + 1) + ": " + text + "\n" + FenString, true);
}

function InitFEN(startingFEN) {
    var ii, jj, cc, color, castlingRookCol, fullMoveNumber;

    var FenString = typeof (startingFEN) != "string" ? FenStringStart : startingFEN.replace(/\\/g, "/").replace(/[^a-zA-Z0-9\s\/-]/g, " ").replace(/(^\s*|\s*$)/g, "").replace(/\s+/g, " ");

    for (ii = 0; ii < 8; ++ii) {
        for (jj = 0; jj < 8; ++jj) {
            Board[ii][jj] = 0;
        }
    }

    StartPly = 0;
    MoveCount = StartPly;
    MoveColor = StartPly % 2;

    var newEnPassant = false;
    var newEnPassantCol;
    CastlingLong = [0, 0];
    CastlingShort = [7, 7];
    InitialHalfMoveClock = 0;

    HistVar[StartPly] = 0;
    HistNull[StartPly] = 0;

    if (FenString == FenStringStart) {
        for (color = 0; color < 2; color++) {
            //                      K  Q  N     B     R     p
            PieceType[color] = [1, 2, 5, 5, 4, 4, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6];
            PieceCol[color] = [4, 3, 1, 6, 2, 5, 0, 7, 0, 1, 2, 3, 4, 5, 6, 7];
            PieceMoveCounter[color] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
            PieceRow[color] = color ? [7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6] :
                [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1];
            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 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) {
                    myAlertFEN(FenString, "piece placement: each row needs 8 squares");
                    InitFEN();
                    return;
                }
                ii = 0;
                jj--;
            }
            if (ii == 8) {
                myAlertFEN(FenString, "piece placement: each row needs 8 squares");
                InitFEN();
                return;
            }
            if (!isNaN(cc)) {
                ii += parseInt(cc, 10);
                if ((ii < 0) || (ii > 8)) {
                    myAlertFEN(FenString, "piece placement: each row needs 8 squares");
                    InitFEN();
                    return;
                }
            }
            if (cc === PiecesArr[0].toUpperCase()) {
                if (PieceType[0][0] != -1) {
                    myAlertFEN(FenString, "piece placement: more than 1 white king");
                    InitFEN();
                    return;
                }
                PieceType[0][0] = 1;
                PieceCol[0][0] = ii;
                PieceRow[0][0] = jj;
                ii++;
            } else if (cc === PiecesArr[0].toLowerCase()) {
                if (PieceType[1][0] != -1) {
                    myAlertFEN(FenString, "piece placement: more than 1 black king");
                    InitFEN();
                    return;
                }
                PieceType[1][0] = 1;
                PieceCol[1][0] = ii;
                PieceRow[1][0] = jj;
                ii++;
            }
            for (kk = 1; kk < 6; kk++) {
                if (cc === PiecesArr[kk].toUpperCase()) {
                    if (nn == 16) {
                        myAlertFEN(FenString, "piece placement: more than 16 white pieces");
                        InitFEN();
                        return;
                    }
                    PieceType[0][nn] = kk + 1;
                    PieceCol[0][nn] = ii;
                    PieceRow[0][nn] = jj;
                    nn++;
                    ii++;
                } else if (cc === PiecesArr[kk].toLowerCase()) {
                    if (mm == 16) {
                        myAlertFEN(FenString, "piece placement: more than 16 black pieces");
                        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)) {
            myAlertFEN(FenString, "piece placement: string termination issue");
            InitFEN();
            return;
        }
        if ((PieceType[0][0] == -1) || (PieceType[1][0] == -1)) {
            myAlertFEN(FenString, "missing king");
            InitFEN();
            return;
        }
        if (ll == FenString.length) {
            FenString += " w " + assumedCastleRights() + " - 0 1";
            ll++;
        }
        cc = FenString.charAt(ll++);
        if ((cc == "w") || (cc == "b")) {
            if (cc == "b") {
                StartPly += 1;
                MoveColor = 1;
            }
        } else {
            myAlertFEN(FenString, "invalid active color");
        }

        // 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) {
            myAlertFEN(FenString, "missing castling availability");
            FenString += " " + assumedCastleRights() + " - 0 1";
            ll++;
        }
        CastlingLong = [-1, -1];
        CastlingShort = [-1, -1];
        cc = FenString.charAt(ll++);
        while (cc != " ") {
            if (cc === PiecesArr[0].toUpperCase()) {
                for (CastlingShort[0] = 7; CastlingShort[0] > PieceCol[0][0]; CastlingShort[0]--) {
                    if (Board[CastlingShort[0]][0] == 3) {
                        break;
                    }
                }
                if (CastlingShort[0] <= PieceCol[0][0]) {
                    myAlertFEN(FenString, "missing castling rook " + cc);
                    CastlingShort[0] = -1;
                }
            } else if (cc === PiecesArr[1].toUpperCase()) {
                for (CastlingLong[0] = 0; CastlingLong[0] < PieceCol[0][0]; CastlingLong[0]++) {
                    if (Board[CastlingLong[0]][0] == 3) {
                        break;
                    }
                }
                if (CastlingLong[0] >= PieceCol[0][0]) {
                    myAlertFEN(FenString, "missing castling rook " + cc);
                    CastlingLong[0] = -1;
                }
            } else if (cc === PiecesArr[0].toLowerCase()) {
                for (CastlingShort[1] = 7; CastlingShort[1] > PieceCol[1][0]; CastlingShort[1]--) {
                    if (Board[CastlingShort[1]][7] == -3) {
                        break;
                    }
                }
                if (CastlingShort[1] <= PieceCol[1][0]) {
                    myAlertFEN(FenString, "missing castling rook " + cc);
                    CastlingShort[1] = -1;
                }
            } else if (cc === PiecesArr[1].toLowerCase()) {
                for (CastlingLong[1] = 0; CastlingLong[1] < PieceCol[1][0]; CastlingLong[1]++) {
                    if (Board[CastlingLong[1]][7] == -3) {
                        break;
                    }
                }
                if (CastlingLong[1] >= PieceCol[1][0]) {
                    myAlertFEN(FenString, "missing castling rook " + cc);
                    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]) {
                        CastlingShort[color] = castlingRookCol;
                    }
                    if (castlingRookCol < PieceCol[color][0]) {
                        CastlingLong[color] = castlingRookCol;
                    }
                } else {
                    myAlertFEN(FenString, "missing castling rook " + cc);
                }
            }
            cc = ll < FenString.length ? FenString.charAt(ll++) : " ";
        }

        if (ll >= FenString.length) {
            myAlertFEN(FenString, "missing en passant square");
            FenString += " - 0 1";
            ll++;
        }
        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) {
            myAlertFEN(FenString, "missing halfmove clock");
            FenString += " 0 1";
            ll++;
        }
        InitialHalfMoveClock = 0;
        cc = FenString.charAt(ll++);
        while (cc != " ") {
            if (isNaN(cc)) {
                myAlertFEN(FenString, "invalid halfmove clock");
                break;
            }
            InitialHalfMoveClock = InitialHalfMoveClock * 10 + parseInt(cc, 10);
            cc = ll < FenString.length ? FenString.charAt(ll++) : " ";
        }
        if (ll >= FenString.length) {
            myAlertFEN(FenString, "missing fullmove number");
            FenString += " 1";
            ll++;
        }

        fullMoveNumber = 0;
        cc = FenString.charAt(ll++);
        while (cc != " ") {
            if (isNaN(cc)) {
                myAlertFEN(FenString, "invalid fullmove number");
                fullMoveNumber = 1;
                break;
            }
            fullMoveNumber = fullMoveNumber * 10 + parseInt(cc, 10);
            cc = ll < FenString.length ? FenString.charAt(ll++) : " ";
        }
        if (fullMoveNumber === 0) {
            myAlertFEN(FenString, "invalid fullmove 0 set to 1");
            fullMoveNumber = 1;
        }
        StartPly += 2 * (fullMoveNumber - 1);

        if (IsCheck(PieceCol[MoveColor ? 0 : 1][0], PieceRow[MoveColor ? 0 : 1][0], MoveColor ? 0 : 1)) {
            myAlertFEN(FenString, (MoveColor ? "Black" : "White") + " to move checking " + (MoveColor ? "white" : "black") + " king");
        }

        HistEnPassant[StartPly] = newEnPassant;
        HistEnPassantCol[StartPly] = newEnPassantCol;
        HistNull[StartPly] = 0;
        HistVar[StartPly] = 0;
    }
}

// castling rights assuming kings and rooks starting positions as in normal chess
function assumedCastleRights() {
    var ii, rights = "";
    if ((PieceRow[0][0] === 0) && (PieceCol[0][0] === 4)) {
        for (ii = 0; ii < PieceType[0].length; ii++) {
            if ((PieceType[0][ii] === 3) && (PieceRow[0][ii] === 0) && (PieceCol[0][ii] === 7)) {
                rights += PiecesArr[0].toUpperCase();
            } else if ((PieceType[0][ii] === 3) && (PieceRow[0][ii] === 0) && (PieceCol[0][ii] === 0)) {
                rights += PiecesArr[1].toUpperCase();
            }
        }
    }
    if ((PieceRow[1][0] === 7) && (PieceCol[1][0] === 4)) {
        for (ii = 0; ii < PieceType[1].length; ii++) {
            if ((PieceType[1][ii] === 3) && (PieceRow[1][ii] === 7) && (PieceCol[1][ii] === 7)) {
                rights += PiecesArr[0].toLowerCase();
            } else if ((PieceType[1][ii] === 3) && (PieceRow[1][ii] === 7) && (PieceCol[1][ii] === 0)) {
                rights += PiecesArr[1].toLowerCase();
            }
        }
    }
    return rights || "-";
}


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 ColorName = new Array("w", "b");
    var PiecePrefix = new Array("k", "q", "r", "b", "n", "p");
    for (var c = 0; c < 2; ++c) {
        for (var p = 1; p < 7; p++) {
            PieceImg[c][p] = new Image();
            PieceImg[c][p].src = ImagePath + ColorName[c] + PiecePrefix[p - 1] + '.' + 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?
    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?
    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, 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 = pgnHeader[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)) {
            switch (parse[1]) {
                case 'Event':
                    gameEvent[ii] = parse[2];
                    break;
                case 'Site':
                    gameSite[ii] = parse[2];
                    break;
                case 'Round':
                    gameRound[ii] = parse[2];
                    break;
                case 'Date':
                    gameDate[ii] = parse[2];
                    break;
                case 'White':
                    gameWhite[ii] = parse[2];
                    break;
                case 'Black':
                    gameBlack[ii] = parse[2];
                    break;
                case 'Result':
                    gameResult[ii] = parse[2];
                    break;
                case 'SetUp':
                    gameSetUp[ii] = parse[2];
                    break;
                case 'FEN':
                    gameFEN[ii] = parse[2];
                    break;
                case 'WhiteClock':
                    gameInitialWhiteClock[ii] = parse[2];
                    break;
                case 'BlackClock':
                    gameInitialBlackClock[ii] = parse[2];
                    break;
                case 'Variant':
                    gameVariant[ii] = parse[2];
                    break;
                default:
                    break;
            }
        }
    }
    if ((LiveBroadcastDemo) && (numberOfGames > 0)) {
        for (ii = 0; ii < numberOfGames; ++ii) {
            if ((gameDemoLength[ii] === undefined) || (gameDemoLength[ii] === 0)) {
                InitFEN(gameFEN[ii]);
                ParsePGNGameString(pgnGame[ii]);
                gameDemoLength[ii] = PlyNumber;
            }
            if (gameDemoMaxPly[ii] === undefined) {
                gameDemoMaxPly[ii] = 0;
            }
            if ((gameDemoMaxPly[ii] <= gameDemoLength[ii]) && (gameDemoLength[ii] > 0)) {
                gameResult[ii] = '*';
            }
        }
    }
    return;
}


function MoveBackward(diff, scanOnly) {

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

    // reconstruct old position
    for (var thisPly = goFromPly; thisPly > goToPly; --thisPly) {
        CurrentPly--;
        MoveColor = 1 - MoveColor;
        CurrentVar = HistVar[thisPly];
        UndoMove(thisPly);
    }

    if (scanOnly) {
        return;
    }

    synchMoves();

    // old position reconstructed: refresh board
    RefreshBoard();
    HighlightLastMove();

    autoScrollToCurrentMoveIfEnabled();

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

    customFunctionOnMove();
    if (typeof (engineWinOnMove) == "function") {
        engineWinOnMove();
    }
}

function MoveForward(diff, targetVar, scanOnly) {
    var nextVar, nextVarStartPly, move, text;
    var oldVar = -1;

    if (typeof (targetVar) == "undefined") {
        targetVar = CurrentVar;
    }

    // CurrentPly counts from 1, starting position 0
    var goToPly = CurrentPly + parseInt(diff, 10);

    if (goToPly > StartPlyVar[targetVar] + PlyNumberVar[targetVar]) {
        goToPly = StartPlyVar[targetVar] + PlyNumberVar[targetVar];
    }

    // reach to selected move checking legality
    for (var thisPly = CurrentPly; thisPly < goToPly; ++thisPly) {

        if (targetVar !== CurrentVar) {
            for (var ii = 0; ii < PredecessorsVars[targetVar].length; ii++) {
                if (PredecessorsVars[targetVar][ii] === CurrentVar) {
                    break;
                }
            }
            if (ii === PredecessorsVars[targetVar].length) {
                myAlert("error: unknown path to variation " + targetVar + " from " + CurrentVar + " in game " + (currentGame + 1), true);
                return;
            } else {
                nextVarStartPly = StartPlyVar[PredecessorsVars[targetVar][ii + 1]];
                for (ii = ii + 1; ii < PredecessorsVars[targetVar].length - 1; ii++) {
                    if (StartPlyVar[PredecessorsVars[targetVar][ii + 1]] !== StartPlyVar[PredecessorsVars[targetVar][ii]]) {
                        break;
                    }
                }
                nextVar = PredecessorsVars[targetVar][ii];
            }
        } else {
            nextVar = nextVarStartPly = -1;
        }

        if (thisPly === nextVarStartPly) {
            oldVar = CurrentVar;
            CurrentVar = nextVar;
        }

        if (typeof (move = MovesVar[CurrentVar][thisPly]) == "undefined") {
            break;
        }

        if (ParseLastMoveError = !ParseMove(move, thisPly)) {
            text = (Math.floor(thisPly / 2) + 1) + ((thisPly % 2) === 0 ? '. ' : '... ');
            myAlert('error: invalid ply ' + text + move + ' in game ' + (currentGame + 1) + ' variation ' + CurrentVar, true);
            if (thisPly === nextVarStartPly) {
                CurrentVar = oldVar;
            }
            break;
        }
        MoveColor = 1 - MoveColor;

    }

    // new position: update ply count, then refresh board
    CurrentPly = thisPly;

    if (scanOnly) {
        return;
    }

    synchMoves();

    RefreshBoard();
    HighlightLastMove();

    autoScrollToCurrentMoveIfEnabled();

    // autoplay: restart timeout
    if (AutoPlayInterval) {
        clearTimeout(AutoPlayInterval);
        AutoPlayInterval = null;
    }
    if (ParseLastMoveError) {
        SetAutoPlay(false);
    } else if (thisPly == goToPly) {
        if (isAutoPlayOn) {
            if (goToPly < StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]) {
                AutoPlayInterval = setTimeout("MoveForward(1)", Delay);
            } else {
                if (autoplayNextGame && (CurrentVar === 0)) {
                    AutoPlayInterval = setTimeout("AutoplayNextGame()", Delay);
                } else {
                    SetAutoPlay(false);
                }
            }
        }
    }

    customFunctionOnMove();
    if (typeof (engineWinOnMove) == "function") {
        engineWinOnMove();
    }
}

var lastSynchCurrentVar = -1;

function synchMoves() {
    var start, end;
    if (CurrentVar === lastSynchCurrentVar) {
        return;
    }
    Moves = new Array();
    MoveComments = new Array();
    for (var ii = 0; ii < PredecessorsVars[CurrentVar].length; ii++) {
        start = StartPlyVar[PredecessorsVars[CurrentVar][ii]];
        if (ii < PredecessorsVars[CurrentVar].length - 1) {
            end = StartPlyVar[PredecessorsVars[CurrentVar][ii + 1]];
        } else {
            end = StartPlyVar[PredecessorsVars[CurrentVar][ii]] + PlyNumberVar[PredecessorsVars[CurrentVar][ii]];
        }
        for (var jj = start; jj < end; jj++) {
            Moves[jj] = MovesVar[PredecessorsVars[CurrentVar][ii]][jj];
            MoveComments[jj] = MoveCommentsVar[PredecessorsVars[CurrentVar][ii]][jj] || "";
        }
    }
    MoveComments[jj] = MoveCommentsVar[PredecessorsVars[CurrentVar][ii - 1]][jj] || "";
    PlyNumber = StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar] - StartPly;
    lastSynchCurrentVar = CurrentVar;
}


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

function MoveToNextComment(varOnly) {
    for (var ii = CurrentPly + 1; ii <= StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]; ii++) {
        if (MoveComments[ii].match(pgn4webVariationRegExp) || (!varOnly && strippedMoveComment(ii))) {
            GoToMove(ii);
            break;
        }
    }
}

function MoveToPrevComment(varOnly) {
    for (var ii = (CurrentPly - 1); ii >= StartPly; ii--) {
        if ((ii > 0 || CurrentVar > 0) && ii === StartPlyVar[HistVar[ii + 1]]) {
            GoToMove(ii + 1, HistVar[ii]);
            break;
        }
        if (MoveComments[ii].match(pgn4webVariationRegExp) || (!varOnly && strippedMoveComment(ii))) {
            GoToMove(ii);
            break;
        }
    }
}


function OpenGame(gameId) {
    ParsePGNGameString(pgnGame[gameId]);
    currentGame = gameId;
    ParseLastMoveError = false;

    if (LiveBroadcastDemo) {
        if (gameDemoMaxPly[gameId] <= PlyNumber) {
            PlyNumber = PlyNumberVar[0] = gameDemoMaxPly[gameId];
        }
    }

    PrintHTML(false);
}

var CurrentVar = -1;
var lastVarWithNoMoves;
var numberOfVars;
var MovesVar;
var MoveCommentsVar;
var GameHasComments;
var GameHasVariations;
var StartPlyVar;
var PlyNumberVar;
var CurrentVarStack;
var PlyNumberStack;
var PredecessorsVars;

function initVar() {
    MovesVar = new Array();
    MoveCommentsVar = new Array();
    GameHasComments = false;
    GameHasVariations = false;
    StartPlyVar = new Array();
    PlyNumberVar = new Array();
    CurrentVar = -1;
    lastVarWithNoMoves = [false];
    numberOfVars = 0;
    CurrentVarStack = new Array();
    PlyNumber = 1;
    PlyNumberStack = new Array();
    PredecessorsVars = new Array();
    startVar(false);
}

function startVar(isContinuation) {
    if (CurrentVar >= 0) {
        CurrentVarStack.push(CurrentVar);
        PlyNumberStack.push(PlyNumber);
    }
    CurrentVar = numberOfVars++;
    PredecessorsVars[CurrentVar] = CurrentVarStack.slice(0);
    PredecessorsVars[CurrentVar].push(CurrentVar);
    MovesVar[CurrentVar] = new Array();
    MoveCommentsVar[CurrentVar] = new Array();
    if (!isContinuation) {
        if (lastVarWithNoMoves[lastVarWithNoMoves.length - 1]) {
            myAlert("warning: malformed PGN data in game " + (currentGame + 1) + ": variation " + CurrentVar + " starting before parent", true);
        } else {
            PlyNumber -= 1;
        }
    }
    lastVarWithNoMoves.push(true);
    MoveCommentsVar[CurrentVar][StartPly + PlyNumber] = "";
    StartPlyVar[CurrentVar] = StartPly + PlyNumber;
}

function closeVar() {
    if (StartPly + PlyNumber === StartPlyVar[CurrentVar]) {
        myAlert("warning: empty variation " + CurrentVar + " in game " + (currentGame + 1), false);
    } else {
        GameHasVariations = true;
    }
    lastVarWithNoMoves.pop();
    PlyNumberVar[CurrentVar] = StartPly + PlyNumber - StartPlyVar[CurrentVar];
    for (var ii = StartPlyVar[CurrentVar]; ii <= StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]; ii++) {
        if (MoveCommentsVar[CurrentVar][ii]) {
            MoveCommentsVar[CurrentVar][ii] = MoveCommentsVar[CurrentVar][ii].replace(/\s+/g, ' ');
            MoveCommentsVar[CurrentVar][ii] = translateNAGs(MoveCommentsVar[CurrentVar][ii]);
            MoveCommentsVar[CurrentVar][ii] = MoveCommentsVar[CurrentVar][ii].replace(/\s+$/g, '');
        } else {
            MoveCommentsVar[CurrentVar][ii] = '';
        }
    }
    if (CurrentVarStack.length) {
        CurrentVar = CurrentVarStack.pop();
        PlyNumber = PlyNumberStack.pop();
    } else {
        myAlert("error: closeVar error" + " in game " + (currentGame + 1), true);
    }
}

function childrenVars(thisPly, thisVar) {
    if (typeof (thisVar) == "undefined") {
        thisVar = CurrentVar;
    }
    if (typeof (thisPly) == "undefined") {
        thisPly = CurrentPly;
    }
    var children = new Array();
    for (var ii = thisVar; ii < numberOfVars; ii++) {
        if ((ii === thisVar && StartPlyVar[ii] + PlyNumberVar[ii] > thisPly) || (realParentVar(ii) === thisVar && StartPlyVar[ii] === thisPly && PlyNumberVar[ii] > 0)) {
            children.push(ii);
        }
    }
    return children;
}

function realParentVar(childVar) {
    for (var ii = PredecessorsVars[childVar].length - 1; ii > 0; ii--) {
        if (StartPlyVar[PredecessorsVars[childVar][ii]] !== StartPlyVar[PredecessorsVars[childVar][ii - 1]]) {
            return PredecessorsVars[childVar][ii - 1];
        }
    }
    return PredecessorsVars[childVar][ii];
}

function goToNextVariationSibling() {
    if (CurrentPly === StartPly) {
        return false;
    }
    var siblings = childrenVars(CurrentPly - 1, HistVar[CurrentPly - 1]);
    if (siblings.length < 2) {
        return false;
    }
    for (var ii = 0; ii < siblings.length; ii++) {
        if (siblings[ii] === CurrentVar) {
            break;
        }
    }
    if (siblings[ii] !== CurrentVar) {
        return false;
    }
    GoToMove(CurrentPly, siblings[(ii + 1) % siblings.length]);
    return true;
}

function goToFirstChild() {
    var children = childrenVars(CurrentPly, CurrentVar);
    if (children.length < 1) {
        return false;
    }
    if (children[0] === CurrentVar) {
        if (children.length < 2) {
            return false;
        }
        GoToMove(CurrentPly + 1, children[1]);
    } else {
        GoToMove(CurrentPly + 1, children[0]);
    }
    return true;
}

function ParsePGNGameString(gameString) {
    var ii, start, end, move, moveCount, needle, commentStart, commentEnd, isContinuation;

    var ssRep, ss = gameString, ssComm;
    ss = ss.replace(pgn4webVariationRegExpGlobal, "[%_pgn4web_variation_ $1]");
    // empty variations to comments
    while ((ssRep = ss.replace(/\((([\?!+#\s]|\$\d+|{[^}]*})*)\)/g, ' $1 ')) !== ss) {
        ss = ssRep;
    }
    ss = ss.replace(/^\s/, '');
    ss = ss.replace(/\s$/, '');

    initVar();

    PlyNumber = 0;

    for (start = 0; start < ss.length; 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 (MoveCommentsVar[CurrentVar][StartPly + PlyNumber]) {
                    MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ' ';
                }
                MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += translateNAGs(ss.substring(commentStart, commentEnd).replace(/(^\s*|\s*$)/, ''));
                start = commentEnd - 1;
                break;

            case '!':
            case '?':
                commentStart = start;
                commentEnd = commentStart + 1;
                while ('!?'.indexOf(ss.charAt(commentEnd)) >= 0) {
                    commentEnd++;
                    if (commentEnd >= ss.length) {
                        break;
                    }
                }
                if (MoveCommentsVar[CurrentVar][StartPly + PlyNumber]) {
                    MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ' ';
                }
                MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ss.substring(commentStart, commentEnd);
                start = commentEnd - 1;
                break;

            case '{':
                commentStart = start + 1;
                commentEnd = ss.indexOf('}', start + 1);
                if (commentEnd < 0) {
                    myAlert('error: missing end comment } in game ' + (currentGame + 1), true);
                    commentEnd = ss.length;
                }
                if (MoveCommentsVar[CurrentVar][StartPly + PlyNumber]) {
                    MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ' ';
                }
                ssComm = translateNAGs(ss.substring(commentStart, commentEnd).replace(/(^\s*|\s*$)/, ''));
                MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ssComm;
                GameHasComments = GameHasComments || ssComm.replace(/\[%[^\]]*\]\s*/g, '').replace(basicNAGs, '').replace(/^\s+$/, '') !== '';
                start = commentEnd;
                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;
                }
                start = commentEnd;
                break;

            case ';':
                commentStart = start + 1;
                commentEnd = ss.indexOf('\n', start + 1);
                if (commentEnd < 0) {
                    commentEnd = ss.length;
                }
                if (MoveCommentsVar[CurrentVar][StartPly + PlyNumber]) {
                    MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ' ';
                }
                ssComm = translateNAGs(ss.substring(commentStart, commentEnd).replace(/(^\s*|\s*$)/, ''));
                MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ssComm;
                GameHasComments = GameHasComments || ssComm.replace(/\[%[^\]]*\]\s*/g, '').replace(basicNAGs, '').replace(/^\s+$/, '') !== '';
                start = commentEnd;
                break;

            case '(':
                if (isContinuation = (ss.charAt(start + 1) == '*')) {
                    start += 1;
                }
                MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ' [%pgn4web_variation ' + numberOfVars + '] ';
                startVar(isContinuation);
                break;

            case ')':
                closeVar();
                break;

            default:

                needle = new Array('1-0', '0-1', '1/2-1/2', '*');
                for (ii = 0; ii < needle.length; ii++) {
                    if (ss.indexOf(needle[ii], start) == start) {
                        if (CurrentVar === 0) {
                            end = ss.length;
                        } else {
                            end = start + needle[ii].length;
                            if (MoveCommentsVar[CurrentVar][StartPly + PlyNumber]) {
                                MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += ' ';
                            }
                            MoveCommentsVar[CurrentVar][StartPly + PlyNumber] += needle[ii];
                        }
                        start = end;
                        break;
                    }
                }
                if (start == ss.length) {
                    break;
                }

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

                if ((end = start + ss.substr(start).search(/[\s${;!?()]/)) < start) {
                    end = ss.length;
                }
                move = ss.substring(start, end);
                MovesVar[CurrentVar][StartPly + PlyNumber] = CleanMove(move);
                lastVarWithNoMoves[lastVarWithNoMoves.length - 1] = false;
                if (ss.charAt(end) == ' ') {
                    start = end;
                } else {
                    start = end - 1;
                }
                if (!MovesVar[CurrentVar][StartPly + PlyNumber].match(/^[\s+#]*$/)) { // to cope with malsformed PGN data
                    PlyNumber++;
                    MoveCommentsVar[CurrentVar][StartPly + PlyNumber] = '';
                }
                break;
        }
    }

    if (CurrentVar !== 0) {
        myAlert("error: ParsePGNGameString ends with CurrentVar " + CurrentVar + " in game " + (currentGame + 1), true);
        while (CurrentVar > 0) {
            closeVar();
        }
    }

    StartPlyVar[0] = StartPly;
    PlyNumberVar[0] = PlyNumber;

    GameHasComments = GameHasComments || GameHasVariations;

    lastSynchCurrentVar = -1;
}

var NAGstyle = 'default';
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] = '+/=';
NAG[16] = 'White has a moderate advantage'; // NAG[17] = '+/-';
NAG[18] = 'White has a decisive advantage'; // NAG[19] = '+-';
NAG[20] = 'White has a crushing advantage'; // NAG[21] = '+-';
NAG[22] = 'White is in zugzwang'; // NAG[23] = '(.)';
NAG[24] = 'White has a slight space advantage'; // NAG[25] = '()';
NAG[26] = 'White has a moderate space advantage'; // NAG[27] = '()';
NAG[28] = 'White has a decisive space advantage'; // NAG[29] = '()';
NAG[30] = 'White has a slight time (development) advantage'; // NAG[31] = '@';
NAG[32] = 'White has a moderate time (development) advantage'; // NAG[33] = '@';
NAG[34] = 'White has a decisive time (development) advantage'; // NAG[35] = '@';
NAG[36] = 'White has the initiative'; // NAG[37] = '|^';
NAG[38] = 'White has a lasting initiative'; // NAG[39] = '|^';
NAG[40] = 'White has the attack'; // NAG[41] = '->';
NAG[42] = 'White has insufficient compensation for material deficit';
NAG[44] = 'White has sufficient compensation for material deficit'; // NAG[45] = '=/~';
NAG[46] = 'White has more than adequate compensation for material deficit'; // NAG[47] = '=/~';
NAG[48] = 'White has a slight center control advantage'; // NAG[49] = '[+]';
NAG[50] = 'White has a moderate center control advantage'; // NAG[51] = '[+]';
NAG[52] = 'White has a decisive center control advantage'; // NAG[53] = '[+]';
NAG[54] = 'White has a slight kingside control advantage'; // NAG[55] = '>>';
NAG[56] = 'White has a moderate kingside control advantage'; // NAG[57] = '>>';
NAG[58] = 'White has a decisive kingside control advantage'; // NAG[59] = '>>';
NAG[60] = 'White has a slight queenside control advantage'; // NAG[61] = '<<';
NAG[62] = 'White has a moderate queenside control advantage'; // NAG[63] = '<<';
NAG[64] = 'White has a decisive queenside control advantage'; // NAG[65] = '<<';
NAG[66] = 'White has a vulnerable first rank';
NAG[68] = 'White has a well protected first rank';
NAG[70] = 'White has a poorly protected king';
NAG[72] = 'White has a well protected king';
NAG[74] = 'White has a poorly placed king';
NAG[76] = 'White has a well placed king';
NAG[78] = 'White has a very weak pawn structure';
NAG[80] = 'White has a moderately weak pawn structure';
NAG[82] = 'White has a moderately strong pawn structure';
NAG[84] = 'White has a very strong pawn structure';
NAG[86] = 'White has poor knight placement';
NAG[88] = 'White has good knight placement';
NAG[90] = 'White has poor bishop placement';
NAG[92] = 'White has good bishop placement';
NAG[94] = 'White has poor rook placement';
NAG[96] = 'White has good rook placement';
NAG[98] = 'White has poor queen placement';
NAG[100] = 'White has good queen placement';
NAG[102] = 'White has poor piece coordination';
NAG[104] = 'White has good piece coordination';
NAG[106] = 'White has played the opening very poorly';
NAG[108] = 'White has played the opening poorly';
NAG[110] = 'White has played the opening well';
NAG[112] = 'White has played the opening very well';
NAG[114] = 'White has played the middlegame very poorly';
NAG[116] = 'White has played the middlegame poorly';
NAG[118] = 'White has played the middlegame well';
NAG[120] = 'White has played the middlegame very well';
NAG[122] = 'White has played the ending very poorly';
NAG[124] = 'White has played the ending poorly';
NAG[126] = 'White has played the ending well';
NAG[128] = 'White has played the ending very well';
NAG[130] = 'White has slight counterplay'; // NAG[131] = '<=>';
NAG[132] = 'White has moderate counterplay'; // NAG[133] = '<=>';
NAG[134] = 'White has decisive counterplay'; // NAG[135] = '<=>';
NAG[136] = 'White has moderate time control pressure'; // NAG[137] = '(+)';
NAG[138] = 'White has severe time control pressure'; // NAG[139] = '(+)';

for (i = 14; i < 139; i += 2) {
    NAG[i + 1] = NAG[i].replace("White", "Black");
}

function translateNAGs(comment) {
    var matches = comment.match(/\$+[0-9]+/g);
    if (matches) {
        for (var ii = 0; ii < matches.length; ii++) {
            var nag = matches[ii].substr(1);
            if (NAG[nag] !== undefined) {
                comment = comment.replace(new RegExp("\\$+" + nag + "(?!\\d)"), NAG[nag]);
            }
        }
    }
    return comment;
}

function ParseMove(move, plyCount) {
    var ii, ll;
    var rem;
    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;
    mvIsNull = 0;

    if (typeof (move) == "undefined") {
        return false;
    }

    HistEnPassant[plyCount + 1] = false;
    HistEnPassantCol[plyCount + 1] = -1;

    if (move.indexOf('--') === 0) {
        mvIsNull = 1;
        CheckLegality('--', plyCount);
        return true;
    }

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

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

    rem = rem.replace(/-/g, '');
    // get piece and origin square: mark captures ('x' is there)
    ll = rem.length;
    if (ll > 4) {
        return false;
    }
    mvPiece = -1; // make sure mvPiece is properly assigned later
    if (ll === 0) {
        mvPiece = 6;
    } else {
        for (ii = 5; ii > 0; ii--) {
            if (rem.charAt(0) == PiecesArr[ii - 1]) {
                mvPiece = ii;
                break;
            }
        }
        if (mvPiece == -1) {
            if (columnsLetters.toLowerCase().indexOf(rem.charAt(0)) >= 0) {
                mvPiece = 6;
            }
        }
        if (mvPiece == -1) {
            return false;
        }
        if (rem.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;
            } else {
                mvFromCol = move.charCodeAt(ll - 2 - mvCapture) - 97;
                if ((mvFromCol < 0) || (mvFromCol > 7)) {
                    mvFromCol = -1;
                }
            }
        }

        if ((ll > 1) && (!mvCapture) && (mvFromCol == -1) && (mvFromRow == -1)) {
            return false;
        }
        if ((mvPiece == 6) && (!mvCapture) && (mvFromCol == -1) && (mvFromRow == -1)) {
            return false;
        }
    }

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

    mvPieceOnTo = mvPiece;
    if (mvPiece == 6) {
        // 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)) {
            var newPiece = move.charAt(ii + 1);
            if (newPiece == PiecesArr[1]) {
                mvPieceOnTo = 2;
            } else if (newPiece == PiecesArr[2]) {
                mvPieceOnTo = 3;
            } else if (newPiece == PiecesArr[3]) {
                mvPieceOnTo = 4;
            } else if (newPiece == PiecesArr[4]) {
                mvPieceOnTo = 5;
            }
            if (mvPieceOnTo != mvPiece) {
                mvIsPromotion = 1;
            }
        }
        if ((mvToRow == 7 * (1 - MoveColor)) ? !mvIsPromotion : mvIsPromotion) {
            return false;
        }
    }

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

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

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

function SetGameSelectorOptions(head, num, chEvent, chSite, chRound, chWhite, chBlack, chResult, chDate) {
    if (typeof (head) == "string") {
        gameSelectorHead = head;
    }
    gameSelectorNum = (num === true);
    gameSelectorChEvent = Math.max(Math.min(chEvent, 32) || 0, 0) || 0;
    gameSelectorChSite = Math.max(Math.min(chSite, 32) || 0, 0) || 0;
    gameSelectorChRound = Math.max(Math.min(chRound, 32) || 0, 0) || 0;
    gameSelectorChWhite = Math.max(Math.min(chWhite, 32) || 0, 0) || 0;
    gameSelectorChBlack = Math.max(Math.min(chBlack, 32) || 0, 0) || 0;
    gameSelectorChResult = Math.max(Math.min(chResult, 32) || 0, 0) || 0;
    gameSelectorChDate = Math.max(Math.min(chDate, 32) || 0, 0) || 0;
}

var clickedSquareInterval = null;

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

function reset_after_click(ii, jj, oldClass, newClass) {
    var theObj = document.getElementById('tcol' + jj + 'trow' + ii);
    if (theObj) {
        // square class changed again by pgn4web already: dont touch it anymore e.g. autoplay
        if (theObj.className == newClass) {
            theObj.className = oldClass;
        }
        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
    var newlinesRegExp = new RegExp("[\n\r]", "gm");
    var searchExpressionRegExp = new RegExp(searchExpression, "im");
    // at start currentGame might still be -1
    var thisCurrentGame = (currentGame < 0) || (currentGame >= numberOfGames) ? 0 : currentGame;
    var needle = fullPgnGame(thisCurrentGame);
    if (includeCurrent && needle.replace(newlinesRegExp, " ").match(searchExpressionRegExp)) {
        return thisCurrentGame;
    }
    var delta = backward ? -1 : +1;
    for (var thisGame = (thisCurrentGame + delta + numberOfGames) % numberOfGames; thisGame != thisCurrentGame; thisGame = (thisGame + delta + numberOfGames) % numberOfGames) {
        needle = fullPgnGame(thisGame);
        if (needle.replace(newlinesRegExp, " ").match(searchExpressionRegExp)) {
            return thisGame;
        }
    }
    return false;
}

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

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

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

var chessMovesRegExp = new RegExp("\\b((\\d+(\\.{1,3}|\\s)\\s*)?((([KQRBN][a-h1-8]?)|[a-h])?x?[a-h][1-8](=[QRNB])?|O-O-O|O-O)\\b[!?+#]*)", "g");

function fixCommentForDisplay(comment) {
    return comment.replace(chessMovesRegExp, '<SPAN CLASS="commentMove">$1</SPAN>');
}

var tableSize = 0;
var textSelectOptions = '';

function PrintHTML(forceBoardUpdate) {
    var ii, jj, text, theObj, squareId, imageId, squareCoord, squareTitle, numText, textSO;

    // chessboard

    if (theObj = document.getElementById("GameBoard")) {
        if (forceBoardUpdate || !document.getElementById("boardTable")) {
            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) {
                text += '<TR>';
                for (jj = 0; jj < 8; ++jj) {
                    squareId = 'tcol' + jj + 'trow' + ii;
                    imageId = 'img_' + 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 += '<IMG SRC="' + ClearImg.src + '" CLASS="pieceImage" STYLE="border: none; display: block; vertical-align: middle;" ONCLICK="boardOnClick[' + jj + '][' + ii + '](this, event);" ID="' + imageId + '" TITLE="' + squareTitle + '" ' + 'ONFOCUS="this.blur()" /></TD>';
                }
                text += '</TR>';
            }
            text += '</TABLE>';

            theObj.innerHTML = text;
        }
    }

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

    // control buttons

    if (theObj = document.getElementById("GameButtons")) {
        var numButtons = 5;
        var spaceSize = 3;
        var buttonSize = (tableSize - spaceSize * (numButtons - 1)) / numButtons;
        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="clickedBbtn(this,event);" 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="clickedBbtn(this,event);" 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="clickedBbtn(this,event);" 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="clickedBbtn(this,event);" 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="clickedBbtn(this,event);" ONFOCUS="this.blur();"></TD></TR></TABLE></FORM>';

        theObj.innerHTML = text;
    }

    // game selector

    if (theObj = document.getElementById("GameSelector")) {
        if (firstStart) {
            textSelectOptions = '';
        }
        if (numberOfGames < 2) {
            while (theObj.firstChild) {
                theObj.removeChild(theObj.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 CLASS="optionSelectControl" value=-1>';

                var blanks = '';
                for (ii = 0; ii < 32; ii++) {
                    blanks += ' ';
                }
                var headDisplay = (gameSelectorNum ? blanks.substring(0, gameSelectorNumLenght) + '  ' : '') + gameSelectorHead;
                text += headDisplay.replace(/ /g, '&nbsp;');

                for (ii = 0; ii < numberOfGames; ii++) {
                    textSelectOptions += '<OPTION CLASS="optionSelectControl" 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) + ' ';
                    }
                    textSelectOptions += textSO.replace(/ /g, '&nbsp;');
                }
                text += textSelectOptions.replace(/&(amp|lt|gt);/g, '&amp;$1;') + '</SELECT></FORM>'; // see function simpleHtmlentities()
                theObj.innerHTML = text;
            }
        }
    }

    // game event

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

    // game round

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

    // game site

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

    // game date

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

    // game white

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

    // game black

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

    // game result

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

    // game text

    if (theObj = document.getElementById("GameText")) {
        variationTextDepth = -1;
        text = '<SPAN ID="ShowPgnText">' + variationTextFromId(0);
        +'</SPAN>';
        theObj.innerHTML = text;
    }

    setB1C1F1G1boardShortcuts(); // depend on presence of comments

    // game searchbox

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

function startButton(e) {
    if (e.shiftKey) {
        GoToMove(StartPlyVar[CurrentVar] + (CurrentPly <= StartPlyVar[CurrentVar] + 1 ? 0 : 1));
    } else {
        GoToMove(StartPlyVar[0], 0);
    }
}

function backButton(e) {
    if (e.shiftKey) {
        GoToMove(StartPlyVar[CurrentVar]);
    } else {
        GoToMove(CurrentPly - 1);
    }
}

function forwardButton(e) {
    if (e.shiftKey) {
        if (!goToNextVariationSibling()) {
            GoToMove(CurrentPly + 1);
        }
    } else {
        GoToMove(CurrentPly + 1);
    }
}

function endButton(e) {
    if (e.shiftKey) {
        if (CurrentPly === StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]) {
            goToFirstChild();
        } else {
            GoToMove(StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]);
        }
    } else {
        GoToMove(StartPlyVar[0] + PlyNumberVar[0], 0);
    }
}

function clickedBbtn(t, e) {
    switch (t.id) {
        case "startButton":
            startButton(e);
            break;
        case "backButton":
            backButton(e);
            break;
        case "autoplayButton":
            if (e.shiftKey) {
                goToNextVariationSibling();
            } else {
                SwitchAutoPlay();
            }
            break;
        case "forwardButton":
            forwardButton(e);
            break;
        case "endButton":
            endButton(e);
            break;
        default:
            break;
    }
}

var basicNAGs = /^[\?!+#\s]+(\s|$)/;

function strippedMoveComment(plyNum, varId, addHtmlTags) {
    if (typeof (addHtmlTags) == "undefined") {
        addHtmlTags = false;
        if (typeof (varId) == "undefined") {
            varId = CurrentVar;
        }
    }
    if (!MoveCommentsVar[varId][plyNum]) {
        return "";
    }
    return fixCommentForDisplay(MoveCommentsVar[varId][plyNum]).replace(pgn4webVariationRegExpGlobal, function (m) {
        return variationTextFromTag(m, addHtmlTags);
    }).replace(/\[%[^\]]*\]\s*/g, '').replace(plyNum === StartPlyVar[varId] ? '' : basicNAGs, '').replace(/^\s+$/, '');
}

function basicNAGsMoveComment(plyNum, varId) {
    if (typeof (varId) == "undefined") {
        varId = CurrentVar;
    }
    if (!MoveCommentsVar[varId][plyNum]) {
        return "";
    }
    var thisBasicNAGs = MoveCommentsVar[varId][plyNum].replace(/\[%[^\]]*\]\s*/g, '').match(basicNAGs, '');
    return thisBasicNAGs ? thisBasicNAGs[0].replace(/\s+(?!class=)/gi, '') : '';
}

function variationTextFromTag(variationTag, addHtmlTags) {
    if (typeof (addHtmlTags) == "undefined") {
        addHtmlTags = false;
    }
    var varId = variationTag.replace(pgn4webVariationRegExp, "$1");
    if (isNaN(varId)) {
        myAlert("error: issue parsing variation tag " + variationTag + " in game " + (currentGame + 1), true);
        return "";
    }
    var text = variationTextFromId(varId);
    if (text) {
        if (addHtmlTags) {
            text = '</SPAN>' + text + '<SPAN CLASS="comment">';
        }
    } else {
        text = '';
    }
    return text;
}

var variationTextDepth, printedComment, printedVariation;

function variationTextFromId(varId) {
    var punctChars = ",.;:!?", thisComment;

    if (isNaN(varId) || varId < 0 || varId >= numberOfVars || typeof (StartPlyVar[varId]) == "undefined" || typeof (PlyNumberVar[varId]) == "undefined") {
        myAlert("error: issue parsing variation id " + varId + " in game " + (currentGame + 1), true);
        return "";
    }
    var text = ++variationTextDepth ? ('<SPAN CLASS="variation">' + (printedVariation ? ' ' : '') + (variationTextDepth > 1 ? '(' : '[')) + '</SPAN>' : '';
    printedVariation = false;
    for (var ii = StartPlyVar[varId]; ii < StartPlyVar[varId] + PlyNumberVar[varId]; ii++) {
        printedComment = false;
        if (commentsIntoMoveText && (thisComment = strippedMoveComment(ii, varId, true))) {
            if (commentsOnSeparateLines && variationTextDepth === 0 && ii > StartPlyVar[varId]) {
                text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
            }
            if (printedVariation) {
                if (punctChars.indexOf(thisComment.charAt(0)) == -1) {
                    text += '<SPAN CLASS="variation"> </SPAN>';
                }
            } else {
                printedVariation = variationTextDepth > 0;
            }
            text += '<SPAN CLASS="comment">' + thisComment + '</SPAN>';
            if (commentsOnSeparateLines && variationTextDepth === 0) {
                text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
            }
            printedComment = true;
        }
        if (printedComment || printedVariation) {
            text += '<SPAN CLASS="variation"> </SPAN>';
        }
        printedVariation = true;

        text += printMoveText(ii, varId, (variationTextDepth > 0), ((printedComment) || (ii == StartPlyVar[varId])), true);
    }
    if (commentsIntoMoveText && (thisComment = strippedMoveComment(StartPlyVar[varId] + PlyNumberVar[varId], varId, true))) {
        if (commentsOnSeparateLines && variationTextDepth === 0) {
            text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
        }
        if (printedVariation && (punctChars.indexOf(thisComment.charAt(0)) == -1)) {
            text += '<SPAN CLASS="comment notranslate"> </SPAN>';
        }
        text += '<SPAN CLASS="comment">' + thisComment + '</SPAN>';
        printedComment = true;
    }
    text += variationTextDepth-- ? ('<SPAN CLASS="variation">' + (variationTextDepth ? ')' : ']') + '</SPAN>') : '';
    printedVariation = true;
    return text;
}

function printMoveText(thisPly, thisVar, isVar, hasLeadingNum, hasId) {
    if (typeof (thisVar) == "undefined") {
        thisVar = CurrentVar;
    }
    if (typeof (thisPly) == "undefined") {
        thisPly = CurrentPly;
    }
    var text = '';

    if (thisVar >= numberOfVars || thisPly < StartPlyVar[thisVar] || thisPly > StartPlyVar[thisVar] + PlyNumberVar[thisVar]) {
        return text;
    }

    var moveCount = Math.floor(thisPly / 2) + 1;
    if (thisPly % 2 === 0) {
        text += '<SPAN CLASS="' + (isVar ? 'variation' : 'move') + ' notranslate">' + moveCount + '.&nbsp;</SPAN>';
    } else {
        if (hasLeadingNum) {
            text += '<SPAN CLASS="' + (isVar ? 'variation' : 'move') + ' notranslate">' + moveCount + '...&nbsp;</SPAN>';
        }
    }
    var jj = thisPly + 1;
    text += '<A HREF="javascript:void(0);" ONCLICK="GoToMove(' + jj + ', ' + thisVar + ');" CLASS="' + (isVar ? 'variation' : 'move') + ' notranslate" ';
    if (hasId) {
        text += 'ID="Var' + thisVar + 'Mv' + jj + '" ';
    }
    text += 'ONFOCUS="this.blur();">' + MovesVar[thisVar][thisPly];
    if (commentsIntoMoveText) {
        text += basicNAGsMoveComment(jj, thisVar);
    }
    text += '</A>';

    return text;
}


// undocumented API to autoscroll gane text to show current move
// enable with "enableAutoScrollToCurrentMove(objId)", with objId as the game text container object id
// add onresize="autoScrollToCurrentMoveIfEnabled" to the body tag for autoscrolling on page resize

function enableAutoScrollToCurrentMove(objId) {
    autoScrollToCurrentMove_objId = objId;
}

function disableAutoScrollToCurrentMove() {
    autoScrollToCurrentMove_objId = "";
}

function toggleAutoScrollToCurrentMove(objId) {
    autoScrollToCurrentMove_objId = autoScrollToCurrentMove_objId ? "" : objId;
}

var autoScrollToCurrentMove_objId = "";

function autoScrollToCurrentMoveIfEnabled() {
    autoScrollToCurrentMove(autoScrollToCurrentMove_objId);
}

function objOffsetVeryTop(obj) {
    for (var offset = obj.offsetTop; obj = obj.offsetParent; /* */) {
        offset += obj.offsetTop + obj.clientTop;
    }
    return offset;
}

function autoScrollToCurrentMove(objId) {
    if (!objId) {
        return;
    }
    var theContainerObj = document.getElementById(objId);
    if (theContainerObj) {
        if (CurrentPly == StartPly) {
            theContainerObj.scrollTop = 0;
        } else {
            var theMoveObj = document.getElementById('Var' + CurrentVar + 'Mv' + CurrentPly);
            if (theMoveObj) {
                var theContainerObjOffsetVeryTop = objOffsetVeryTop(theContainerObj);
                var theMoveObjOffsetVeryTop = objOffsetVeryTop(theMoveObj);
                if ((theMoveObjOffsetVeryTop + theMoveObj.offsetHeight > theContainerObjOffsetVeryTop + theContainerObj.scrollTop + theContainerObj.clientHeight) || (theMoveObjOffsetVeryTop < theContainerObjOffsetVeryTop + theContainerObj.scrollTop)) {
                    theContainerObj.scrollTop = theMoveObjOffsetVeryTop - theContainerObjOffsetVeryTop;
                }
            }
        }
    }
}


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

function RefreshBoard() {
    for (var jj = 0; jj < 8; ++jj) {
        for (var ii = 0; ii < 8; ++ii) {
            if (Board[jj][ii] === 0) {
                SetImage(jj, ii, ClearImg.src);
            }
        }
    }
    for (jj = 0; jj < 2; ++jj) {
        for (ii = 0; ii < 16; ++ii) {
            if (PieceType[jj][ii] > 0) {
                SetImage(PieceCol[jj][ii], PieceRow[jj][ii], PieceImg[jj][PieceType[jj][ii]].src);
            }
        }
    }
}

function SetAutoPlay(vv) {
    isAutoPlayOn = vv;

    if (AutoPlayInterval) {
        clearTimeout(AutoPlayInterval);
        AutoPlayInterval = null;
    }

    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 < StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]) {
            AutoPlayInterval = setTimeout("MoveForward(1)", Delay);
        } else {
            if (autoplayNextGame && (CurrentVar === 0)) {
                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";
            }
        }
    }
}


var minAutoplayDelay = 500;
var maxAutoplayDelay = 300000;

function setCustomAutoplayDelay() {
    var newDelaySec = prompt("Enter custom autoplay delay, in seconds, between " + (minAutoplayDelay / 1000) + " and " + (maxAutoplayDelay / 1000) + ":", Math.floor(Delay / 100) / 10);
    if (!isNaN(newDelaySec = parseInt(newDelaySec, 10))) {
        SetAutoplayDelayAndStart(newDelaySec * 1000);
    }
}

function SetAutoplayDelay(vv) {
    if (isNaN(vv = parseInt(vv, 10))) {
        return;
    }
    Delay = Math.min(Math.max(vv, minAutoplayDelay), maxAutoplayDelay);
}

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

function SetLiveBroadcast(delay, alertFlag, demoFlag, stepFlag, endlessFlag) {
    LiveBroadcastDelay = delay; // zero delay: no live broadcast
    LiveBroadcastAlert = (alertFlag === true);
    LiveBroadcastDemo = (demoFlag === true);
    LiveBroadcastSteppingMode = (stepFlag === true);
    LiveBroadcastEndlessMode = (endlessFlag === true);
    setG7A6B6H7boardShortcuts();
}

function SetImage(col, row, image) {
    var trow = IsRotated ? row : 7 - row;
    var tcol = IsRotated ? 7 - col : col;
    var theObj = document.getElementById('img_' + 'tcol' + tcol + 'trow' + trow);
    if ((theObj) && (theObj.src != image)) {
        theObj.src = image;
    }
}

function SetImagePath(path) {
    ImagePath = path;
}

function SwitchAutoPlay() {
    if (!isAutoPlayOn) {
        MoveForward(1);
    }
    SetAutoPlay(!isAutoPlayOn);
}

function StoreMove(thisPly) {

    HistVar[thisPly + 1] = CurrentVar;

    if (HistNull[thisPly] = mvIsNull) {
        return;
    }

    // "square from" history
    HistPieceId[0][thisPly] = mvPieceId;
    HistCol[0][thisPly] = PieceCol[MoveColor][mvPieceId];
    HistRow[0][thisPly] = PieceRow[MoveColor][mvPieceId];
    HistType[0][thisPly] = PieceType[MoveColor][mvPieceId];

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

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

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

    // mark 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: promotion changes 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) {

    if (HistNull[thisPly]) {
        return;
    }

    // moved piece back to original square
    var chgPiece = HistPieceId[0][thisPly];
    Board[PieceCol[MoveColor][chgPiece]][PieceRow[MoveColor][chgPiece]] = 0;

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

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

var pgn4webMaxTouches = 0;
var pgn4webOngoingTouches = new Array();

function pgn4webOngoingTouchIndexById(needle) {
    var id;
    for (var ii = 0; ii < pgn4webOngoingTouches.length; ii++) {
        id = pgn4webOngoingTouches[ii].identifier;
        if (pgn4webOngoingTouches[ii].identifier === needle) {
            return ii;
        }
    }
    return -1;
}

function pgn4web_handleTouchStart(e) {
    e.stopPropagation();
    for (var ii = 0; ii < e.changedTouches.length; ii++) {
        pgn4webMaxTouches++;
        pgn4webOngoingTouches.push({
            identifier: e.changedTouches[ii].identifier,
            clientX: e.changedTouches[ii].clientX,
            clientY: e.changedTouches[ii].clientY
        });
    }
}

function pgn4web_handleTouchMove(e) {
    e.stopPropagation();
    e.preventDefault();
}

function pgn4web_handleTouchEnd(e) {
    e.stopPropagation();
    var jj;
    for (var ii = 0; ii < e.changedTouches.length; ii++) {
        if ((jj = pgn4webOngoingTouchIndexById(e.changedTouches[ii].identifier)) != -1) {
            if (pgn4webOngoingTouches.length == 1) {
                customFunctionOnTouch(e.changedTouches[ii].clientX - pgn4webOngoingTouches[jj].clientX, e.changedTouches[ii].clientY - pgn4webOngoingTouches[jj].clientY);
                pgn4webMaxTouches = 0;
            }
            pgn4webOngoingTouches.splice(jj, 1);
        }
    }
    clearSelectedText();
}

function pgn4web_handleTouchCancel(e) {
    e.stopPropagation();
    var jj;
    for (var ii = 0; ii < e.changedTouches.length; ii++) {
        if ((jj = pgn4webOngoingTouchIndexById(e.changedTouches[ii].identifier)) != -1) {
            pgn4webOngoingTouches.splice(jj, 1);
            if (pgn4webOngoingTouches.length === 0) {
                pgn4webMaxTouches = 0;
            }
        }
    }
    clearSelectedText();
}

function pgn4web_initTouchEvents() {
    var theObj = document.getElementById("GameBoard");
    if (theObj && touchEventEnabled) {
        simpleAddEvent(theObj, "touchstart", pgn4web_handleTouchStart);
        simpleAddEvent(theObj, "touchmove", pgn4web_handleTouchMove);
        simpleAddEvent(theObj, "touchend", pgn4web_handleTouchEnd);
        simpleAddEvent(theObj, "touchleave", pgn4web_handleTouchEnd);
        simpleAddEvent(theObj, "touchcancel", pgn4web_handleTouchCancel);
    }
}

var waitForDoubleLeftTouchTimer = null;

function customFunctionOnTouch(deltaX, deltaY) {
    if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 13) {
        return;
    }
    if (Math.abs(deltaY) > 1.5 * Math.abs(deltaX)) { // vertical up or down
        if (numberOfGames > 1) {
            if ((currentGame === 0) && (deltaY < 0)) {
                Init(numberOfGames - 1);
            } else if ((currentGame === numberOfGames - 1) && (deltaY > 0)) {
                Init(0);
            } else {
                Init(currentGame + sign(deltaY));
            }
        }
    } else if (Math.abs(deltaX) > 1.5 * Math.abs(deltaY)) {
        if (deltaX > 0) { // horizontal right
            if (isAutoPlayOn) {
                GoToMove(StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]);
            } else {
                SwitchAutoPlay();
            }
        } else { // horizontal left
            if (isAutoPlayOn && !waitForDoubleLeftTouchTimer) {
                SwitchAutoPlay();
            } else {
                if (waitForDoubleLeftTouchTimer) {
                    clearTimeout(waitForDoubleLeftTouchTimer);
                    waitForDoubleLeftTouchTimer = null;
                }
                if ((LiveBroadcastDelay > 0) && (CurrentVar === 0) && (CurrentPly === StartPly + PlyNumber)) {
                    waitForDoubleLeftTouchTimer = setTimeout("waitForDoubleLeftTouchTimer = null;", 900);
                    replayPreviousMoves(6);
                } else {
                    GoToMove(StartPlyVar[CurrentVar] + (((CurrentPly <= StartPlyVar[CurrentVar] + 1) || (CurrentVar === 0)) ? 0 : 1));
                }
            }
        }
    }
}

var touchGestures_helpActions = ["chessboard top-down swipe", "chessboard bottom-up swipe", "chessboard left-right swipe", "chessboard right-left swipe"];
var touchGestures_helpText = ["load next game, cycling through", "load previous game, cyclig through", "start autoplay; if autoplay already active: go to variation end or to game end", "stop autoplay; if autoplay not active: go to variation start, then to parent variation, then to game start; if at last move of live broadcast: replay up to 6 previous half-moves, then autoplay forward"];

var touchEventEnabled = true;

function SetTouchEventEnabled(onOff) {
    touchEventEnabled = onOff;
}

function clearSelectedText() {
    if (window.getSelection) {
        if (window.getSelection().empty) {
            window.getSelection().empty();
        } else if (window.getSelection().removeAllRanges) {
            window.getSelection().removeAllRanges();
        }
    } else if (document.selection && document.selection.empty) {
        document.selection.empty();
    }
}

function simpleHtmlentities(text) {
    return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function simpleHtmlentitiesDecode(text) {
    return text.replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
}

