1/*
2 *  pgn4web javascript chessboard
3 *  copyright (C) 2009-2016 Paolo Casaschi
4 *  see README file and http://pgn4web.casaschi.net
5 *  for credits, license and more details
6 */
7
8"use strict";
9
10var pgn4web_version = '3.00';
11
12var pgn4web_project_url = "http://pgn4web.casaschi.net";
13var pgn4web_project_author = "Paolo Casaschi";
14var pgn4web_project_email = "pgn4web@casaschi.net";
15
16var helpWin;
17function displayHelp(section) {
18  if (helpWin && !helpWin.closed) { helpWin.close(); }
19  helpWin = window.open(detectHelpLocation() + (section ? "?" + section : ""), "pgn4web_help", "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
20  if (helpWin && window.focus) { helpWin.focus(); }
21}
22
23
24// empty event APIs to be redefined
25
26function customFunctionOnPgnTextLoad() {}
27function customFunctionOnPgnGameLoad() {}
28function customFunctionOnMove() {}
29function customFunctionOnAlert(msg) {}
30function customFunctionOnCheckLiveBroadcastStatus() {}
31
32// custom header tags APIs for customFunctionOnPgnGameLoad
33
34function customPgnHeaderTag(customTag, htmlElementId, gameNum) {
35  var matches, tag = "";
36  customTag = customTag.replace(/\W+/g, "");
37  if (gameNum === undefined) { gameNum = currentGame; }
38  if ((pgnHeader[gameNum]) && (matches = pgnHeader[gameNum].match('\\[\\s*' + customTag + '\\s*\"([^\"]+)\"\\s*\\]'))) { tag = matches[1]; }
39  if (htmlElementId) {
40    var theObj = document.getElementById(htmlElementId);
41    if ((theObj) && (typeof(theObj.innerHTML) == "string")) { theObj.innerHTML = tag; }
42  }
43  return tag;
44}
45
46// custom comment tags API for customFunctionOnMove
47
48function customPgnCommentTag(customTag, htmlElementId, plyNum, varId) {
49  var matches, tag = "", theObj;
50  customTag = customTag.replace(/\W+/g, "");
51  if (typeof(varId) == "undefined") { varId = 0; }
52  if (typeof(plyNum) == "undefined") { plyNum = CurrentPly; }
53  if ((MoveCommentsVar[varId][plyNum]) && (matches = MoveCommentsVar[varId][plyNum].match('\\[%' + customTag + '\\s+((?:,?(?:"[^"]*"|[^,\\]]*))*)\\s*\\]'))) { tag = matches[1].replace(/\s+$/, ""); }
54  if ((htmlElementId) && (theObj = document.getElementById(htmlElementId)) && (typeof(theObj.innerHTML) == "string")) { theObj.innerHTML = tag; }
55  return tag;
56}
57
58
59function simpleAddEvent(obj, evt, cbk) {
60  if (obj.addEventListener) { obj.addEventListener(evt, cbk, false); }
61  else if (obj.attachEvent) { obj.attachEvent("on" + evt, cbk); } // bugfix: IE8 and below specific
62}
63
64simpleAddEvent(document, "keydown", pgn4web_handleKey_event);
65simpleAddEvent(window, "load", pgn4web_onload_event);
66
67
68function pgn4web_onload_event(e) {
69  pgn4web_onload(e);
70}
71
72function pgn4web_onload(e) {
73  start_pgn4web();
74}
75
76function start_pgn4web() {
77  // keep startup logs at the very first run, otherwise reset
78  if (alertFirstResetLoadingPgn) { alertFirstResetLoadingPgn = false; }
79  else { resetAlert(); }
80  InitImages();
81  createBoard();
82  if (LiveBroadcastDelay > 0) { restartLiveBroadcastTimeout(); }
83  pgn4web_initTouchEvents();
84}
85
86var alertLog;
87var alertLast;
88var alertNum;
89var alertNumSinceReset;
90var fatalErrorNumSinceReset;
91var alertPromptInterval = null;
92var alertPromptOn = false;
93var alertFirstResetLoadingPgn = true;
94
95resetAlert();
96
97function resetAlert() {
98  alertLog = new Array(5);
99  alertLast = alertLog.length - 1;
100  alertNum = alertNumSinceReset = fatalErrorNumSinceReset = 0;
101  stopAlertPrompt();
102  if (!alertFirstResetLoadingPgn) {
103    if (boardIsDefault(debugShortcutSquare)) { boardShortcut(debugShortcutSquare, "pgn4web v" + pgn4web_version + " debug info", null, true); }
104  }
105}
106
107function myAlert(msg, fatalError, doNotPrompt) {
108  alertNum++;
109  alertNumSinceReset++;
110  if (fatalError) { fatalErrorNumSinceReset++; }
111  alertLast = (alertLast + 1) % alertLog.length;
112  alertLog[alertLast] = msg + "\n" + (new Date()).toLocaleString();
113  if (boardIsDefault(debugShortcutSquare)) { boardShortcut(debugShortcutSquare, "pgn4web v" + pgn4web_version + " debug info\n" + alertNum + " alert" + (alertNum > 1 ? "s" : ""), null, true); }
114  if ((!doNotPrompt) && ((LiveBroadcastDelay === 0) || (LiveBroadcastAlert === true)) && (boardIsDefault(debugShortcutSquare))) { startAlertPrompt(); }
115  customFunctionOnAlert(msg);
116}
117
118function startAlertPrompt() {
119  if (alertPromptOn) { return; } // dont start flashing twice
120  if (alertPromptInterval) { clearTimeout(alertPromptInterval); }
121  alertPromptInterval = setTimeout("alertPromptTick(true);", 500);
122}
123
124function stopAlertPrompt() {
125  if (alertPromptInterval) {
126    clearTimeout(alertPromptInterval);
127    alertPromptInterval = null;
128  }
129  if (alertPromptOn) { alertPromptTick(false); }
130}
131
132function alertPromptTick(restart) {
133  if (alertPromptInterval) {
134    clearTimeout(alertPromptInterval);
135    alertPromptInterval = null;
136  }
137  var colRow = colRowFromSquare(debugShortcutSquare);
138  if (!colRow) { return; }
139
140  var alertPromptDelay = 1500; // for alerts before the board is printed
141  var theObj = document.getElementById('tcol' + colRow.col + 'trow' + colRow.row);
142  if (theObj) {
143    if (alertPromptOn) {
144      if ((highlightOption) && ((colFromHighlighted === 0 && rowFromHighlighted === 7) || (colToHighlighted === 0 && rowToHighlighted === 7))) {
145          theObj.className = 'highlightWhiteSquare';
146        } else { theObj.className = 'whiteSquare'; }
147    } else { theObj.className = 'blackSquare'; }
148
149    alertPromptOn = !alertPromptOn;
150    if (alertPromptOn) { alertPromptDelay = 500; }
151    else { alertPromptDelay = 3000; }
152  }
153  if (restart) { alertPromptInterval = setTimeout("alertPromptTick(true);", alertPromptDelay); }
154}
155
156
157function stopEvProp(e) {
158  e.cancelBubble = true;
159  if (e.stopPropagation) { e.stopPropagation(); }
160  if (e.preventDefault) { e.preventDefault(); }
161  return false;
162}
163
164// for onFocus/onBlur textbox events, allowing text typing
165var shortcutKeysWereEnabled = false;
166function disableShortcutKeysAndStoreStatus() {
167  if ((shortcutKeysWereEnabled = shortcutKeysEnabled) === true) {
168    SetShortcutKeysEnabled(false);
169  }
170}
171function restoreShortcutKeysStatus() {
172  if (shortcutKeysWereEnabled === true) { SetShortcutKeysEnabled(true); }
173  shortcutKeysWereEnabled = false;
174}
175
176function customShortcutKey_Shift_0() {}
177function customShortcutKey_Shift_1() {}
178function customShortcutKey_Shift_2() {}
179function customShortcutKey_Shift_3() {}
180function customShortcutKey_Shift_4() {}
181function customShortcutKey_Shift_5() {}
182function customShortcutKey_Shift_6() {}
183function customShortcutKey_Shift_7() {}
184function customShortcutKey_Shift_8() {}
185function customShortcutKey_Shift_9() {}
186
187function pgn4web_handleKey_event(e) {
188  pgn4web_handleKey(e);
189}
190
191var shortcutKeysEnabled = false;
192function pgn4web_handleKey(e) {
193  var keycode, oldPly, oldVar, colRow, colRowList;
194
195  if (!e) { e = window.event; }
196
197  if (e.altKey || e.ctrlKey || e.metaKey) { return true; }
198
199  keycode = e.keyCode;
200
201  // shift-escape always enabled: toggle shortcut keys
202  if (!shortcutKeysEnabled && !(keycode == 27 && e.shiftKey)) { return true; }
203
204  switch (keycode) {
205
206    case  8: // backspace
207    case  9: // tab
208    case 16: // shift
209    case 17: // ctrl
210    case 18: // alt
211    case 32: // space
212    case 33: // page-up
213    case 34: // page-down
214    case 35: // end
215    case 36: // home
216    case 92: // super
217    case 93: // menu
218    case 188: // comma
219      return true;
220
221    case 27: // escape
222      if (e.shiftKey) { interactivelyToggleShortcutKeys(); }
223      else { displayHelp(); }
224      break;
225
226    case 189: // dash
227      if (colRowList = prompt("Enter shortcut square coordinates to click:", "")) {
228        colRowList = colRowList.toUpperCase().replace(/[^A-Z0-9]/g,"");
229        while (colRow = colRowFromSquare(colRowList)) {
230          boardOnClick[colRow.col][colRow.row]({"id": "img_tcol" + colRow.col + "trow" + colRow.row}, e);
231          colRowList = colRowList.substr(2);
232        }
233      }
234      break;
235
236    case 90: // z
237      if (e.shiftKey) { window.open(pgn4web_project_url); }
238      else { displayDebugInfo(); }
239      break;
240
241    case 37: // left-arrow
242    case 74: // j
243      backButton(e);
244      break;
245
246    case 38: // up-arrow
247    case 72: // h
248      startButton(e);
249      break;
250
251    case 39: // right-arrow
252    case 75: // k
253      forwardButton(e);
254      break;
255
256    case 40: // down-arrow
257    case 76: // l
258      endButton(e);
259      break;
260
261    case 73: // i
262      MoveToPrevComment(e.shiftKey);
263      break;
264
265    case 79: // o
266      MoveToNextComment(e.shiftKey);
267      break;
268
269    case 190: // dot
270      if (e.shiftKey) { goToFirstChild(); }
271      else { goToNextVariationSibling(); }
272      break;
273
274    case 85: // u
275      if (e.shiftKey) { undoStackRedo(); }
276      else { undoStackUndo(); }
277      break;
278
279    case 45: // insert
280      undoStackRedo();
281      break;
282
283    case 46: // delete
284      undoStackUndo();
285      break;
286
287    case 83: // s
288      if (e.shiftKey) { searchPgnGame(""); }
289      else { searchPgnGamePrompt(); }
290      break;
291
292    case 13: // enter
293      if (e.shiftKey) { searchPgnGame(lastSearchPgnExpression, true); }
294      else { searchPgnGame(lastSearchPgnExpression); }
295      break;
296
297    case 68: // d
298      if (e.shiftKey) { displayFenData(); }
299      else { displayPgnData(true); }
300      break;
301
302    case 187: // equal
303      SwitchAutoPlay();
304      break;
305
306    case 65: // a
307      GoToMove(CurrentPly + 1);
308      SetAutoPlay(true);
309      break;
310
311    case 48: // 0
312      if (e.shiftKey) { customShortcutKey_Shift_0(); }
313      else { SetAutoPlay(false); }
314      break;
315
316    case 49: // 1
317      if (e.shiftKey) { customShortcutKey_Shift_1(); }
318      else { SetAutoplayDelayAndStart( 1*1000); }
319      break;
320
321    case 50: // 2
322      if (e.shiftKey) { customShortcutKey_Shift_2(); }
323      else { SetAutoplayDelayAndStart( 2*1000); }
324      break;
325
326    case 51: // 3
327      if (e.shiftKey) { customShortcutKey_Shift_3(); }
328      else { SetAutoplayDelayAndStart( 3*1000); }
329      break;
330
331    case 52: // 4
332      if (e.shiftKey) { customShortcutKey_Shift_4(); }
333      else { SetAutoplayDelayAndStart( 4*1000); }
334      break;
335
336    case 53: // 5
337      if (e.shiftKey) { customShortcutKey_Shift_5(); }
338      else { SetAutoplayDelayAndStart( 5*1000); }
339      break;
340
341    case 54: // 6
342      if (e.shiftKey) { customShortcutKey_Shift_6(); }
343      else { SetAutoplayDelayAndStart( 6*1000); }
344      break;
345
346    case 55: // 7
347      if (e.shiftKey) { customShortcutKey_Shift_7(); }
348      else { SetAutoplayDelayAndStart( 7*1000); }
349      break;
350
351    case 56: // 8
352      if (e.shiftKey) { customShortcutKey_Shift_8(); }
353      else { SetAutoplayDelayAndStart( 8*1000); }
354      break;
355
356    case 57: // 9
357      if (e.shiftKey) { customShortcutKey_Shift_9(); }
358      else { setCustomAutoplayDelay(); }
359      break;
360
361    case 81: // q
362      SetAutoplayDelayAndStart(10*1000);
363      break;
364
365    case 87: // w
366      SetAutoplayDelayAndStart(20*1000);
367      break;
368
369    case 69: // e
370      SetAutoplayDelayAndStart(30*1000);
371      break;
372
373    case 82: // r
374      pauseLiveBroadcast();
375      break;
376
377    case 84: // t
378      if (e.shiftKey) { LiveBroadcastSteppingMode = !LiveBroadcastSteppingMode; }
379      else { refreshPgnSource(); }
380      break;
381
382    case 89: // y
383      restartLiveBroadcast();
384      break;
385
386    case 70: // f
387      if (!e.shiftKey || IsRotated) { FlipBoard(); }
388      break;
389
390    case 71: // g
391      SetHighlight(!highlightOption);
392      break;
393
394    case 88: // x
395      randomGameRandomPly();
396      break;
397
398    case 67: // c
399      if (numberOfGames > 1) { Init(Math.floor(Math.random()*numberOfGames)); }
400      break;
401
402    case 86: // v
403      if (numberOfGames > 1) { Init(0); }
404      break;
405
406    case 66: // b
407      Init(currentGame - 1);
408      break;
409
410    case 78: // n
411      Init(currentGame + 1);
412      break;
413
414    case 77: // m
415      if (numberOfGames > 1) { Init(numberOfGames - 1); }
416      break;
417
418    case 80: // p
419      if (e.shiftKey) { SetCommentsOnSeparateLines(!commentsOnSeparateLines); }
420      else { SetCommentsIntoMoveText(!commentsIntoMoveText); }
421      oldPly = CurrentPly;
422      oldVar = CurrentVar;
423      Init();
424      GoToMove(oldPly, oldVar);
425      break;
426
427    default:
428      return true;
429  }
430  return stopEvProp(e);
431}
432
433var boardOnClick = new Array(8);
434var boardTitle = new Array(8);
435var boardDefault = new Array(8);
436for (var col=0; col<8; col++) {
437  boardOnClick[col] = new Array(8);
438  boardTitle[col] = new Array(8);
439  boardDefault[col] = new Array(8);
440}
441clearShortcutSquares("ABCDEFGH", "12345678");
442
443function colRowFromSquare(square) {
444  if ((typeof(square) != "string") || (!square)) { return null; }
445  var col = square.charCodeAt(0) - 65; // 65="A"
446  if ((col < 0) || (col > 7)) { return null; }
447  var row = 56 - square.charCodeAt(1); // 56="8"
448  if ((row < 0) || (row > 7)) { return null; }
449  return { "col": col, "row": row };
450}
451
452function clearShortcutSquares(cols, rows) {
453  if ((typeof(cols) != "string") || (typeof(rows) != "string")) { return; }
454  for (var c=0; c<cols.length; c++) { for (var r=0; r<rows.length; r++) {
455    boardShortcut(cols.charAt(c).toUpperCase()+rows.charAt(r), "", function(t,e){});
456  } }
457}
458
459function boardIsDefault(square) {
460  var colRow = colRowFromSquare(square);
461  if (!colRow) { return false; }
462  return boardDefault[colRow.col][colRow.row];
463}
464
465function boardShortcut(square, title, functionPointer, defaultSetting) {
466  var theObj, colRow = colRowFromSquare(square);
467  if (!colRow) { return; }
468  else { var col = colRow.col; var row = colRow.row; }
469  boardTitle[col][row] = title;
470  if (functionPointer) { boardOnClick[col][row] = functionPointer; }
471  boardDefault[col][row] = defaultSetting ? true : false;
472  if (theObj = document.getElementById('img_tcol' + col + 'trow' + row)) {
473    if (IsRotated) { square = String.fromCharCode(72-col,49+row); }
474    theObj.title = square + (boardTitle[col][row] ? ': ' + boardTitle[col][row] : '');
475  }
476}
477
478// boardShortcut() always assumes 'square' defined as with white on bottom
479
480var debugShortcutSquare = "A8";
481
482
483boardShortcut("A8", "pgn4web v" + pgn4web_version + " debug info", function(t,e){ displayDebugInfo(); }, true);
484
485boardShortcut("B8", "show current position FEN string", function(t,e){ displayFenData(); }, true);
486
487boardShortcut("C8", "show current game PGN source data", function(t,e){ if (e.shiftKey) { savePgnData(true); } else { displayPgnData(true); } }, true);
488
489boardShortcut("D8", "show all games PGN source data", function(t,e){ if (e.shiftKey) { savePgnData(); } else { displayPgnData(); } }, true);
490
491boardShortcut("E8", "search help", function(t,e){ displayHelp("search_tool"); }, true);
492
493boardShortcut("F8", "shortcut keys help", function(t,e){ displayHelp("shortcut_keys"); }, true);
494
495boardShortcut("G8", "shortcut squares help", function(t,e){ displayHelp(e.shiftKey ? "informant_symbols" : "shortcut_squares"); }, true);
496
497boardShortcut("H8", "pgn4web help", function(t,e){ displayHelp(e.shiftKey ? "credits_and_license" : ""); }, true);
498
499boardShortcut("A7", "pgn4web website", function(t,e){ window.open(pgn4web_project_url); }, true);
500
501boardShortcut("B7", "undo last chessboard position update", function(t,e){ undoStackUndo(); }, true);
502
503boardShortcut("C7", "redo last undo", function(t,e){ undoStackRedo(); }, true);
504
505boardShortcut("D7", "toggle highlight last move", function(t,e){ SetHighlight(!highlightOption); }, true);
506
507boardShortcut("E7", "flip board", function(t,e){ if (!e.shiftKey || IsRotated) { FlipBoard(); } }, true);
508
509boardShortcut("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);
510
511boardShortcut("G7", "", function(t,e){}, true); // see setG7A6B6H6...
512
513boardShortcut("H7", "toggle enabling shortcut keys", function(t,e){ interactivelyToggleShortcutKeys(); }, true);
514
515boardShortcut("A6", "", function(t,e){}, true); // see setG7A6B6H6...
516
517boardShortcut("B6", "", function(t,e){}, true); // see setG7A6B6H6...
518
519boardShortcut("C6", "search previous finished game", function(t,e){ searchPgnGame('\\[\\s*Result\\s*"(?!\\*"\\s*\\])', true); });
520
521boardShortcut("D6", "search previous unfinished game", function(t,e){ searchPgnGame('\\[\\s*Result\\s*"\\*"\\s*\\]', true); });
522
523boardShortcut("E6", "search next unfinished game", function(t,e){ searchPgnGame('\\[\\s*Result\\s*"\\*"\\s*\\]', false); }, true);
524
525boardShortcut("F6", "search next finished game", function(t,e){ searchPgnGame('\\[\\s*Result\\s*"(?!\\*"\\s*\\])', false); }, true);
526
527boardShortcut("G6", "", function(t,e){}, true);
528
529boardShortcut("H6", "", function(t,e){}, true); // see setG7A6B6H6...
530
531boardShortcut("A5", "repeat last search backward", function(t,e){ searchPgnGame(lastSearchPgnExpression, true); }, true);
532
533boardShortcut("B5", "search prompt", function(t,e){ if (e.shiftKey) { searchPgnGame(""); } else { searchPgnGamePrompt(); } }, true);
534
535boardShortcut("C5", "repeat last search", function(t,e){ searchPgnGame(lastSearchPgnExpression); }, true);
536
537boardShortcut("D5", "search previous win result", function(t,e){ searchPgnGame('\\[\\s*Result\\s*"(1-0|0-1)"\\s*\\]', true); }, true);
538
539boardShortcut("E5", "search next win result", function(t,e){ searchPgnGame('\\[\\s*Result\\s*"(1-0|0-1)"\\s*\\]', false); }, true);
540
541boardShortcut("F5", "", function(t,e){}, true);
542
543boardShortcut("G5", "", function(t,e){}, true);
544
545boardShortcut("H5", "", function(t,e){}, true);
546
547boardShortcut("A4", "search previous event", function(t,e){ searchPgnGame('\\[\\s*Event\\s*"(?!' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\])', true); }, true);
548
549boardShortcut("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);
550
551boardShortcut("C4", "search previous game of same black player", function(t,e){ searchPgnGame('\\[\\s*' + (e.shiftKey ? 'White' : 'Black') + '\\s*"' + fixRegExp(gameBlack[currentGame]) + '"\\s*\\]', true); }, true);
552
553boardShortcut("D4", "search previous game of same white player", function(t,e){ searchPgnGame('\\[\\s*' + (e.shiftKey ? 'Black' : 'White') + '\\s*"' + fixRegExp(gameWhite[currentGame]) + '"\\s*\\]', true); }, true);
554
555boardShortcut("E4", "search next game of same white player", function(t,e){ searchPgnGame('\\[\\s*' + (e.shiftKey ? 'Black' : 'White') + '\\s*"' + fixRegExp(gameWhite[currentGame]) + '"\\s*\\]', false); }, true);
556
557boardShortcut("F4", "search next game of same black player", function(t,e){ searchPgnGame('\\[\\s*' + (e.shiftKey ? 'White' : 'Black') + '\\s*"' + fixRegExp(gameBlack[currentGame]) + '"\\s*\\]', false); }, true);
558
559boardShortcut("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);
560
561boardShortcut("H4", "search next event", function(t,e){ searchPgnGame('\\[\\s*Event\\s*"(?!' + fixRegExp(gameEvent[currentGame]) + '"\\s*\\])', false); }, true);
562
563boardShortcut("A3", "load first game", function(t,e){ if (numberOfGames > 1) { Init(0); } }, true);
564
565boardShortcut("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);
566
567boardShortcut("C3", "load previous game", function(t,e){ Init(currentGame - 1); }, true);
568
569boardShortcut("D3", "load random game", function(t,e){ if (numberOfGames > 1) { Init(Math.floor(Math.random()*numberOfGames)); } }, true);
570
571boardShortcut("E3", "load random game at random position", function(t,e){ randomGameRandomPly(); }, true);
572
573boardShortcut("F3", "load next game", function(t,e){ Init(currentGame + 1); }, true);
574
575boardShortcut("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);
576
577boardShortcut("H3", "load last game", function(t,e){ if (numberOfGames > 1) { Init(numberOfGames - 1); } }, true);
578
579boardShortcut("A2", "stop autoplay", function(t,e){ SetAutoPlay(e.shiftKey); }, true);
580
581boardShortcut("B2", "toggle autoplay", function(t,e){ SwitchAutoPlay(); }, true);
582
583boardShortcut("C2", "autoplay 1 second", function(t,e){ SetAutoplayDelayAndStart((e.shiftKey ? 10 : 1)*1000); }, true);
584
585boardShortcut("D2", "autoplay 2 seconds", function(t,e){ SetAutoplayDelayAndStart((e.shiftKey ? 20 : 2)*1000); }, true);
586
587boardShortcut("E2", "autoplay 5 seconds", function(t,e){ SetAutoplayDelayAndStart((e.shiftKey ? 50 : 5)*1000); }, true);
588
589boardShortcut("F2", "autoplay custom delay", function(t,e){ setCustomAutoplayDelay(); }, true);
590
591boardShortcut("G2", "replay up to 6 previous half-moves, then autoplay forward", function(t,e){ replayPreviousMoves(e.shiftKey ? 10 : 6); }, true);
592
593boardShortcut("H2", "replay the previous half-move, then autoplay forward", function(t,e){ replayPreviousMoves(e.shiftKey ? 3 : 1); }, true);
594
595boardShortcut("A1", "go to game start", function(t,e){ startButton(e); }, true);
596
597boardShortcut("B1", "", function(t,e){}, true); // see setB1C1F1G1...
598
599boardShortcut("C1", "", function(t,e){}, true); // see setB1C1F1G1...
600
601boardShortcut("D1", "move backward", function(t,e){ backButton(e); }, true);
602
603boardShortcut("E1", "move forward", function(t,e){ forwardButton(e); }, true);
604
605boardShortcut("F1", "", function(t,e){}, true); // see setB1C1F1G1...
606
607boardShortcut("G1", "", function(t,e){}, true); // see setB1C1F1G1...
608
609boardShortcut("H1", "go to game end", function(t,e){ endButton(e); }, true);
610
611
612setG7A6B6H7boardShortcuts();
613
614function setG7A6B6H7boardShortcuts() {
615  if (LiveBroadcastDelay > 0) {
616    if (boardIsDefault("G7")) { boardShortcut("G7", "", function(t,e){}, true); }
617    if (boardIsDefault("A6")) { boardShortcut("A6", "pause live broadcast automatic games refresh", function(t,e){ pauseLiveBroadcast(); }, true); }
618    if (boardIsDefault("B6")) { boardShortcut("B6", "restart live broadcast automatic games refresh", function(t,e){ restartLiveBroadcast(); }, true); }
619    if (boardIsDefault("H6")) { boardShortcut("H6", "force live broadcast games refresh", function(t,e){ refreshPgnSource(); }, true); }
620  } else {
621    if (boardIsDefault("G7")) { boardShortcut("G7", "toggle autoplay next game", function(t,e){ SetAutoplayNextGame(!autoplayNextGame); }, true); }
622    if (boardIsDefault("A6")) { boardShortcut("A6", "", function(t,e){}, true); }
623    if (boardIsDefault("B6")) { boardShortcut("B6", "", function(t,e){}, true); }
624    if (boardIsDefault("H6")) { boardShortcut("H6", "", function(t,e){}, true); }
625  }
626}
627
628setB1C1F1G1boardShortcuts();
629
630function setB1C1F1G1boardShortcuts() {
631  if (commentsIntoMoveText && GameHasComments) {
632    if (boardIsDefault("B1")) { boardShortcut("B1", "find previous comment or variation", function(t,e){ if (e.shiftKey) { GoToMove(CurrentPly - 10); } else { MoveToPrevComment(); } }, true); }
633    if (boardIsDefault("G1")) { boardShortcut("G1", "find next comment or variation", function(t,e){ if (e.shiftKey) { GoToMove(CurrentPly + 10); } else { MoveToNextComment(); } }, true); }
634  } else {
635    if (boardIsDefault("B1")) { boardShortcut("B1", "move 10 half-moves backward", function(t,e){ GoToMove(CurrentPly - 10); }, true); }
636    if (boardIsDefault("G1")) { boardShortcut("G1", "move 10 half-moves forward", function(t,e){ GoToMove(CurrentPly + 10); }, true); }
637  }
638  if (commentsIntoMoveText && GameHasVariations) {
639    if (boardIsDefault("C1")) { boardShortcut("C1", "go to parent variation", function(t,e){ if (e.shiftKey) { GoToMove(CurrentPly - 6); } else { GoToMove(StartPlyVar[CurrentVar]); } }, true); }
640    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); }
641  } else {
642    if (boardIsDefault("C1")) { boardShortcut("C1", "move 6 half-moves backward", function(t,e){ GoToMove(CurrentPly - 6); }, true); }
643    if (boardIsDefault("F1")) { boardShortcut("F1", "move 6 half-moves forward", function(t,e){ GoToMove(CurrentPly + 6); }, true); }
644  }
645}
646
647
648var deciles = new Array(11);
649function calculateDeciles() {
650  for (var ii=0; ii<deciles.length; ii++) {
651    deciles[ii] = Math.round((numberOfGames - 1) * ii / (deciles.length - 1));
652  }
653}
654
655function replayPreviousMoves(numPlies) {
656  var thisPly = numPlies ? CurrentPly - numPlies : StartPly;
657  if (thisPly < StartPlyVar[CurrentVar]) {
658    thisPly = StartPlyVar[CurrentVar] + (CurrentVar === 0 ? 0 : 1);
659  }
660  if (thisPly !== CurrentPly) { GoToMove(thisPly); }
661  SetAutoPlay(true);
662}
663
664function detectJavascriptLocation(jsre) {
665  if (typeof(jsre) == "undefined") { jsre = new RegExp("pgn4web(|-compacted|\.min)\.js$", ""); }
666  var e = document.getElementsByTagName("script");
667  for (var i=0; i<e.length; i++) {
668    if ((e[i].src) && (e[i].src.match(jsre))) {
669      return e[i].src;
670    }
671  }
672  return "";
673}
674
675function detectHelpLocation() {
676  return detectJavascriptLocation().replace(/pgn4web(|-compacted|\.min)\.js$/, "pgn4web-help.html");
677}
678
679function detectBaseLocation() {
680  var e = document.getElementsByTagName("base");
681  for (var i=0; i<e.length; i++) {
682    if (e[i].href) { return e[i].href; }
683  }
684  return "";
685}
686
687
688var debugWin;
689function displayDebugInfo() {
690  var theObj;
691  var bu = detectBaseLocation();
692  var ju = detectJavascriptLocation();
693  stopAlertPrompt();
694  var dbg1 = 'pgn4web: version=' + pgn4web_version + ' homepage=' + pgn4web_project_url + '\n\nHTMLURL: length=' + location.href.length + ' url=';
695  var dbg2 = location.href.length < 100 ? location.href : (location.href.substring(0,99) + '...');
696  var dbg3 = (bu ? '\nBASEURL: url=' + bu : '') + (ju != 'pgn4web.js' ? '\nJSURL: url=' + ju : '');
697  if (pgnUrl) {
698    dbg3 += '\nPGNURL: url=' + pgnUrl;
699  } else if (theObj = document.getElementById("pgnText")) {
700    dbg3 += '\nPGNTEXT: length=' + (theObj.tagName.toLowerCase() == 'textarea' ? theObj.value.length : '?');
701  }
702  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;
703  if ((typeof(gameVariant[currentGame]) !== "undefined") && (gameVariant[currentGame].match(/^\s*(|chess|normal|standard)\s*$/i) === null)) {
704    dbg3 += '\nVARIANT: ' + gameVariant[currentGame];
705  }
706  if (LiveBroadcastDelay > 0) {
707    dbg3 += '\n\nLIVEBROADCAST: status=' + liveStatusDebug() + (LiveBroadcastEndlessMode ? '/endless ' : '') + ' ticker=' + LiveBroadcastTicker + ' delay=' + LiveBroadcastDelay + 'm' + '\n' + 'refreshed: ' + LiveBroadcastLastRefreshedLocal + '\n' + 'received: ' + LiveBroadcastLastReceivedLocal + '\n' + 'modified (server time): ' + LiveBroadcastLastModified_ServerTime();
708  }
709  if (typeof(engineWinCheck) == "function") {
710    dbg3 += '\n\nANALYSIS: ' + (engineWinCheck() ? 'board=connected ' + engineWin.customDebugInfo() : 'board=disconnected');
711  }
712  var thisInfo = customDebugInfo();
713  if (thisInfo) { dbg3 += '\n\nCUSTOM: ' + thisInfo; }
714  dbg3 += '\n\nALERTLOG: fatalnew=' + fatalErrorNumSinceReset + ' new=' + alertNumSinceReset + ' shown=' + Math.min(alertNum, alertLog.length) + ' total=' + alertNum + '\n--';
715  if (alertNum > 0) {
716    for (var ii = 0; ii<alertLog.length; ii++) {
717      if (alertLog[(alertNum - 1 - ii) % alertLog.length] === undefined) { break; }
718      else { dbg3 += "\n" + alertLog[(alertNum - 1 - ii) % alertLog.length] + "\n--"; }
719    }
720  }
721  if (confirm(dbg1 + dbg2 + dbg3 + '\n\nclick OK to show this debug info in a browser window for cut and paste')) {
722    if (debugWin && !debugWin.closed) { debugWin.close(); }
723    debugWin = window.open("", "pgn4web_debug_data", "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
724    if (debugWin) {
725      debugWin.document.open("text/html", "replace");
726      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>");
727      debugWin.document.close();
728      if (window.focus) { debugWin.focus(); }
729    }
730  }
731  alertNumSinceReset = fatalErrorNumSinceReset = 0;
732}
733
734function liveStatusDebug() {
735  if (LiveBroadcastEnded) { return "ended"; }
736  if (LiveBroadcastPaused) { return "paused"; }
737  if (LiveBroadcastStarted) { return "started"; }
738  return "waiting";
739}
740
741function customDebugInfo() { return ""; }
742
743var pgnWin;
744function displayPgnData(oneGameOnly) {
745  if (pgnWin && !pgnWin.closed) { pgnWin.close(); }
746  pgnWin = window.open("", "pgn4web_pgn_data", "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
747  if (pgnWin) {
748    var text = "<html><head><title>pgn4web PGN source</title><link rel='icon' sizes='16x16' href='pawn.ico' /></head><body>\n<pre>\n";
749    if (oneGameOnly) { text += fullPgnGame(currentGame) + "\n\n"; }
750    else { for (var ii = 0; ii < numberOfGames; ++ii) { text += fullPgnGame(ii) + "\n\n"; } }
751    text += "\n</pre>\n</body></html>";
752    pgnWin.document.open("text/html", "replace");
753    pgnWin.document.write(text);
754    pgnWin.document.close();
755    if (window.focus) { pgnWin.focus(); }
756  }
757}
758
759function savePgnData(oneGameOnly) {
760  if (pgnUrl && !oneGameOnly) { location.href = pgnUrl; }
761  else {
762    displayPgnData(oneGameOnly); // fallback on displayPgnData for now
763  }
764}
765
766function CurrentFEN() {
767  var thisFEN = "";
768
769  var emptySquares = 0;
770  for (var row=7; row>=0; row--) {
771    for (var col=0; col<=7; col++) {
772      if (Board[col][row] === 0) { emptySquares++; }
773      else {
774        if (emptySquares) {
775          thisFEN += emptySquares;
776          emptySquares = 0;
777        }
778        if (Board[col][row] > 0) { thisFEN += PiecesArr[Board[col][row]-1].toUpperCase(); }
779        else if (Board[col][row] < 0) { thisFEN += PiecesArr[-Board[col][row]-1].toLowerCase(); }
780      }
781    }
782    if (emptySquares) {
783      thisFEN += emptySquares;
784      emptySquares = 0;
785    }
786    if (row>0) { thisFEN += "/"; }
787  }
788
789  thisFEN += CurrentPly%2 ? " b" : " w";
790
791  // castling availability: always in the KQkq form
792  // note: wrong FEN for Chess960 positions with inner castling rook
793  var CastlingFEN = "";
794  if (RookForOOCastling(0) !== null) { CastlingFEN += PiecesArr[0].toUpperCase(); }
795  if (RookForOOOCastling(0) !== null) { CastlingFEN += PiecesArr[1].toUpperCase(); }
796  if (RookForOOCastling(1) !== null) { CastlingFEN += PiecesArr[0].toLowerCase(); }
797  if (RookForOOOCastling(1) !== null) { CastlingFEN += PiecesArr[1].toLowerCase(); }
798  thisFEN += " " + (CastlingFEN || "-");
799
800  if (HistEnPassant[CurrentPly]) {
801    thisFEN += " " + String.fromCharCode(HistEnPassantCol[CurrentPly] + 97);
802    thisFEN += CurrentPly%2 ? "3" : "6";
803  } else { thisFEN += " -"; }
804
805  var HalfMoveClock = InitialHalfMoveClock;
806  for (var thisPly = StartPly; thisPly < CurrentPly; thisPly++) {
807    if ((HistType[0][thisPly] == 6) || (HistPieceId[1][thisPly] >= 16)) { HalfMoveClock = 0; }
808    else { HalfMoveClock++; }
809  }
810  thisFEN += " " + HalfMoveClock;
811
812  thisFEN += " " + (Math.floor(CurrentPly/2)+1);
813
814  return thisFEN;
815}
816
817var fenWin;
818function displayFenData(addGametext) {
819  if (fenWin && !fenWin.closed) { fenWin.close(); }
820
821  var thisFEN = CurrentFEN();
822
823  var movesStr = "";
824  var lineStart = 0;
825  if (addGametext) {
826    for (var thisPly = CurrentPly; thisPly <= StartPly + PlyNumber; thisPly++) {
827      var addStr = "";
828      if (thisPly == StartPly + PlyNumber) {
829        addStr = (CurrentVar ? "*" : gameResult[currentGame] || "*");
830      } else {
831        if (thisPly%2 === 0) { addStr = (Math.floor(thisPly/2)+1) + ". "; }
832        else if (thisPly == CurrentPly) { addStr = (Math.floor(thisPly/2)+1) + "... "; }
833        addStr += Moves[thisPly];
834      }
835      if (movesStr.length + addStr.length + 1 > lineStart + 80) {
836        lineStart = movesStr.length;
837        movesStr += "\n" + addStr;
838      } else {
839        if (movesStr.length > 0) { movesStr += " "; }
840        movesStr += addStr;
841      }
842    }
843  }
844
845  fenWin = window.open("", "pgn4web_fen_data", "resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no");
846  if (fenWin) {
847    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";
848    if (addGametext) {
849      text += "[Event \""  + ((CurrentVar ? "" : gameEvent[currentGame])  || "?") + "\"]\n";
850      text += "[Site \""   + ((CurrentVar ? "" : gameSite[currentGame])   || "?") + "\"]\n";
851      text += "[Date \""   + ((CurrentVar ? "" : gameDate[currentGame])   || "????.??.??") + "\"]\n";
852      text += "[Round \""  + ((CurrentVar ? "" : gameRound[currentGame])  || "?") + "\"]\n";
853      text += "[White \""  + ((CurrentVar ? "" : gameWhite[currentGame])  || "?") + "\"]\n";
854      text += "[Black \""  + ((CurrentVar ? "" : gameBlack[currentGame])  || "?") + "\"]\n";
855      text += "[Result \"" + ((CurrentVar ? "" : gameResult[currentGame]) || "*") + "\"]\n";
856    }
857    if ((thisFEN != FenStringStart) || (!addGametext)) {
858      text += "[SetUp \"1\"]\n" + "[FEN \"" + thisFEN + "\"]\n";
859    }
860    if (gameVariant[currentGame] !== "") { text += "[Variant \"" + gameVariant[currentGame] + "\"]\n"; }
861    if (addGametext) { text += "\n" + movesStr + "\n"; }
862    text += "</pre>\n</body></html>";
863    fenWin.document.open("text/html", "replace");
864    fenWin.document.write(text);
865    fenWin.document.close();
866    if (window.focus) { fenWin.focus(); }
867  }
868}
869
870
871var pgnHeader = new Array();
872var pgnGame = new Array();
873var numberOfGames = -1;
874var currentGame = -1;
875
876var firstStart = true;
877
878var gameDate = new Array();
879var gameWhite = new Array();
880var gameBlack = new Array();
881var gameEvent = new Array();
882var gameSite = new Array();
883var gameRound = new Array();
884var gameResult = new Array();
885var gameSetUp = new Array();
886var gameFEN = new Array();
887var gameInitialWhiteClock = new Array();
888var gameInitialBlackClock = new Array();
889var gameVariant = new Array();
890
891var highlightedMoveId = "";
892
893var isAutoPlayOn = false;
894var AutoPlayInterval = null;
895var Delay = 1000; // milliseconds
896var autostartAutoplay = false;
897var autoplayNextGame = false;
898
899var initialGame = 1;
900var initialVariation = 0;
901var initialHalfmove = 0;
902var alwaysInitialHalfmove = false;
903
904var LiveBroadcastInterval = null;
905var LiveBroadcastDelay = 0; // minutes
906var LiveBroadcastAlert = false;
907var LiveBroadcastDemo = false;
908var LiveBroadcastStarted = false;
909var LiveBroadcastEnded = false;
910var LiveBroadcastPaused = false;
911var LiveBroadcastTicker = 0;
912var LiveBroadcastGamesRunning = 0;
913var LiveBroadcastLastModified = new Date(0); // default to epoch start
914var LiveBroadcastLastModifiedHeader = LiveBroadcastLastModified.toUTCString();
915var LiveBroadcastLastReceivedLocal = 'unavailable';
916var LiveBroadcastLastRefreshedLocal = 'unavailable';
917var LiveBroadcastPlaceholderEvent = 'live chess broadcast';
918var LiveBroadcastPlaceholderPgn = '[Event "' + LiveBroadcastPlaceholderEvent + '"]';
919var gameDemoMaxPly = new Array();
920var gameDemoLength = new Array();
921var LiveBroadcastSteppingMode = false;
922var LiveBroadcastEndlessMode = false;
923
924var ParseLastMoveError = false;
925
926var castleRook = -1;
927var mvCapture = 0;
928var mvIsCastling = 0;
929var mvIsPromotion = 0;
930var mvFromCol = -1;
931var mvFromRow = -1;
932var mvToCol = -1;
933var mvToRow = -1;
934var mvPiece = -1;
935var mvPieceId = -1;
936var mvPieceOnTo = -1;
937var mvCaptured = -1;
938var mvCapturedId = -1;
939var mvIsNull = 0;
940
941var Board = new Array(8);
942for (var i=0; i<8; ++i) { Board[i] = new Array(8); }
943
944// HistCol, HistRow: move history up to last replayed ply
945// HistCol[0], HistRow[0]: "square from"; 0..7, 0..7 from A1
946// HistCol[1], HistRow[1]: castling/capture
947// HistCol[2], HistRow[2]: "square to"; 0..7, 0..7 from A1
948
949var HistCol = new Array(3);
950var HistRow = new Array(3);
951var HistPieceId = new Array(2);
952var HistType = new Array(2);
953var HistVar = new Array();
954
955var PieceCol = new Array(2);
956var PieceRow = new Array(2);
957var PieceType = new Array(2);
958var PieceMoveCounter = new Array(2);
959
960for (i=0; i<2; ++i) {
961  PieceCol[i] = new Array(16);
962  PieceRow[i] = new Array(16);
963  PieceType[i] = new Array(16);
964  PieceMoveCounter[i] = new Array(16);
965  HistType[i] = new Array();
966  HistPieceId[i] = new Array();
967}
968
969for (i=0; i<3; ++i) {
970  HistCol[i] = new Array();
971  HistRow[i] = new Array();
972}
973
974var HistEnPassant = new Array();
975HistEnPassant[0] = false;
976var HistEnPassantCol = new Array();
977HistEnPassantCol[0] = -1;
978
979var HistNull = new Array();
980HistNull[0] = 0;
981
982var PiecesArr = "KQRBNP".split("");
983var FenStringStart = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
984var columnsLetters = "ABCDEFGH";
985var InitialHalfMoveClock = 0;
986
987var PieceImg = new Array(new Array(6), new Array(6));
988var ClearImg;
989
990var ImagePath = 'images';
991var ImagePathOld = null;
992var imageType = 'png';
993var defaultImagesSize = 40;
994
995var highlightOption = true;
996
997var commentsIntoMoveText = true;
998var commentsOnSeparateLines = false;
999
1000var pgnUrl = '';
1001
1002var CastlingLong = new Array(2);
1003var CastlingShort = new Array(2);
1004var Moves = new Array();
1005var MoveComments = new Array();
1006
1007var MoveColor;
1008var MoveCount;
1009var PlyNumber;
1010var StartPly;
1011var CurrentPly;
1012
1013var IsRotated = false;
1014
1015var pgnHeaderTagRegExp       = /\[\s*(\w+)\s*"([^"]*)"\s*\]/;
1016var pgnHeaderTagRegExpGlobal = /\[\s*(\w+)\s*"([^"]*)"\s*\]/g;
1017var pgnHeaderBlockRegExp     = /\s*(\[\s*\w+\s*"[^"]*"\s*\]\s*)+/;
1018
1019var emptyPgnHeader = '[Event ""]\n[Site ""]\n[Date ""]\n[Round ""]\n[White ""]\n[Black ""]\n[Result ""]\n';
1020var alertPgn = emptyPgnHeader + "\n{error: click on the top left chessboard square for debug info}";
1021
1022var pgn4webVariationRegExp       = /\[%pgn4web_variation (\d+)\]/;
1023var pgn4webVariationRegExpGlobal = /\[%pgn4web_variation (\d+)\]/g;
1024
1025var gameSelectorHead = ' &middot;&middot;&middot;';
1026var gameSelectorMono = true;
1027var gameSelectorNum = false;
1028var gameSelectorNumLenght = 0;
1029var gameSelectorChEvent = 0;
1030var gameSelectorChSite = 0;
1031var gameSelectorChRound = 0;
1032var gameSelectorChWhite = 15;
1033var gameSelectorChBlack = 15;
1034var gameSelectorChResult = 0;
1035var gameSelectorChDate = 10;
1036
1037function CheckLegality(what, plyCount) {
1038  var retVal, thisCol;
1039
1040  if (what == '--') {
1041    StoreMove(plyCount);
1042    return true;
1043  }
1044
1045  // castling
1046  if (what == 'O-O') {
1047    if (!CheckLegalityOO()) { return false; }
1048    for (thisCol = PieceCol[MoveColor][0]; thisCol < 7; thisCol++) {
1049      if (IsCheck(thisCol, MoveColor*7, MoveColor)) { return false; }
1050    }
1051    StoreMove(plyCount);
1052    return true;
1053  } else if (what == 'O-O-O') {
1054    if (!CheckLegalityOOO()) { return false; }
1055    for (thisCol = PieceCol[MoveColor][0]; thisCol > 1; thisCol--) {
1056      if (IsCheck(thisCol, MoveColor*7, MoveColor)) { return false; }
1057    }
1058    StoreMove(plyCount);
1059    return true;
1060  }
1061
1062  // capture: "square to" occupied by opposite color piece (except en-passant)
1063  // promotion: "square to" moved piece different from piece
1064  if (!mvCapture) {
1065    if (Board[mvToCol][mvToRow] !== 0) { return false; }
1066  }
1067  if ((mvCapture) && (Color(Board[mvToCol][mvToRow]) != 1-MoveColor)) {
1068    if ((mvPiece != 6) || (!HistEnPassant[plyCount]) || (HistEnPassantCol[plyCount] != mvToCol) || (mvToRow != 5-3*MoveColor)) { return false; }
1069  }
1070  if (mvIsPromotion) {
1071    if (mvPiece != 6) { return false; }
1072    if (mvPieceOnTo >= 6) { return false; }
1073    if (mvToRow != 7*(1-MoveColor)) { return false; }
1074  }
1075
1076  // piece move: which same type piece could move there?
1077  for (var pieceId = 0; pieceId < 16; ++pieceId) {
1078    if (PieceType[MoveColor][pieceId] == mvPiece) {
1079      if (mvPiece == 1) { retVal = CheckLegalityKing(pieceId); }
1080      else if (mvPiece == 2) { retVal = CheckLegalityQueen(pieceId); }
1081      else if (mvPiece == 3) { retVal = CheckLegalityRook(pieceId); }
1082      else if (mvPiece == 4) { retVal = CheckLegalityBishop(pieceId); }
1083      else if (mvPiece == 5) { retVal = CheckLegalityKnight(pieceId); }
1084      else if (mvPiece == 6) { retVal = CheckLegalityPawn(pieceId); }
1085      if (retVal) {
1086        mvPieceId = pieceId;
1087        // board updated: king check?
1088        StoreMove(plyCount);
1089        if (!IsCheck(PieceCol[MoveColor][0], PieceRow[MoveColor][0], MoveColor)) { return true; }
1090        else { UndoMove(plyCount); }
1091      }
1092    }
1093  }
1094  return false;
1095}
1096
1097function CheckLegalityKing(thisKing) {
1098  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisKing])) { return false; }
1099  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisKing])) { return false; }
1100  if (Math.abs(PieceCol[MoveColor][thisKing]-mvToCol) > 1) { return false; }
1101  if (Math.abs(PieceRow[MoveColor][thisKing]-mvToRow) > 1) { return false; }
1102  return true;
1103}
1104
1105function CheckLegalityQueen(thisQueen) {
1106  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisQueen])) { return false; }
1107  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisQueen])) { return false; }
1108  if (((PieceCol[MoveColor][thisQueen]-mvToCol) * (PieceRow[MoveColor][thisQueen]-mvToRow) !== 0) && (Math.abs(PieceCol[MoveColor][thisQueen]-mvToCol) != Math.abs(PieceRow[MoveColor][thisQueen]-mvToRow))) { return false; }
1109  if (!CheckClearWay(thisQueen)) { return false; }
1110  return true;
1111}
1112
1113function CheckLegalityRook(thisRook) {
1114  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisRook])) { return false; }
1115  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisRook])) { return false; }
1116  if ((PieceCol[MoveColor][thisRook]-mvToCol) * (PieceRow[MoveColor][thisRook]-mvToRow) !== 0) { return false; }
1117  if (!CheckClearWay(thisRook)) { return false; }
1118  return true;
1119}
1120
1121function CheckLegalityBishop(thisBishop) {
1122  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisBishop])) { return false; }
1123  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisBishop])) { return false; }
1124  if (Math.abs(PieceCol[MoveColor][thisBishop]-mvToCol) != Math.abs(PieceRow[MoveColor][thisBishop]-mvToRow)) { return false; }
1125  if (!CheckClearWay(thisBishop)) { return false; }
1126  return true;
1127}
1128
1129function CheckLegalityKnight(thisKnight) {
1130  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisKnight])) { return false; }
1131  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisKnight])) { return false; }
1132  if (Math.abs(PieceCol[MoveColor][thisKnight]-mvToCol) * Math.abs(PieceRow[MoveColor][thisKnight]-mvToRow) != 2) { return false; }
1133  return true;
1134}
1135
1136function CheckLegalityPawn(thisPawn) {
1137  if ((mvFromCol >= 0) && (mvFromCol != PieceCol[MoveColor][thisPawn])) { return false; }
1138  if ((mvFromRow >= 0) && (mvFromRow != PieceRow[MoveColor][thisPawn])) { return false; }
1139  if (Math.abs(PieceCol[MoveColor][thisPawn]-mvToCol) != mvCapture) { return false; }
1140  if (mvCapture) {
1141    if (PieceRow[MoveColor][thisPawn]-mvToRow != 2*MoveColor-1) { return false; }
1142  } else {
1143    if (PieceRow[MoveColor][thisPawn]-mvToRow == 4*MoveColor-2) {
1144      if (PieceRow[MoveColor][thisPawn] != 1+5*MoveColor) { return false; }
1145      if (Board[mvToCol][mvToRow+2*MoveColor-1] !== 0) { return false; }
1146    } else {
1147      if (PieceRow[MoveColor][thisPawn]-mvToRow != 2*MoveColor-1) { return false; }
1148    }
1149  }
1150  return true;
1151}
1152
1153function RookForOOCastling(color) {
1154  if (CastlingShort[color] < 0) { return null; }
1155  if (PieceMoveCounter[color][0] > 0) { return null; }
1156
1157  var legal = false;
1158  for (var thisRook = 0; thisRook < 16; thisRook++) {
1159    if ((PieceCol[color][thisRook] == CastlingShort[color]) && (PieceCol[color][thisRook] > PieceCol[color][0]) && (PieceRow[color][thisRook] == color*7) && (PieceType[color][thisRook] == 3)) {
1160      legal = true;
1161      break;
1162    }
1163  }
1164  if (!legal) { return null; }
1165  if (PieceMoveCounter[color][thisRook] > 0) { return null; }
1166
1167  return thisRook;
1168}
1169
1170function CheckLegalityOO() {
1171
1172  var thisRook = RookForOOCastling(MoveColor);
1173  if (thisRook === null) { return false; }
1174
1175  // check no piece between king and rook
1176  // clear king/rook squares for Chess960
1177  Board[PieceCol[MoveColor][0]][MoveColor*7] = 0;
1178  Board[PieceCol[MoveColor][thisRook]][MoveColor*7] = 0;
1179  var col = PieceCol[MoveColor][thisRook];
1180  if (col < 6) { col = 6; }
1181  while ((col > PieceCol[MoveColor][0]) || (col >= 5)) {
1182    if (Board[col][MoveColor*7] !== 0) { return false; }
1183    --col;
1184  }
1185  castleRook = thisRook;
1186  return true;
1187}
1188
1189function RookForOOOCastling(color) {
1190  if (CastlingLong[color] < 0) { return null; }
1191  if (PieceMoveCounter[color][0] > 0) { return null; }
1192
1193  var legal = false;
1194  for (var thisRook = 0; thisRook < 16; thisRook++) {
1195    if ((PieceCol[color][thisRook] == CastlingLong[color]) && (PieceCol[color][thisRook] < PieceCol[color][0]) && (PieceRow[color][thisRook] == color*7) && (PieceType[color][thisRook] == 3)) {
1196      legal = true;
1197      break;
1198    }
1199  }
1200  if (!legal) { return null; }
1201  if (PieceMoveCounter[color][thisRook] > 0) { return null; }
1202
1203  return thisRook;
1204}
1205
1206function CheckLegalityOOO() {
1207
1208  var thisRook = RookForOOOCastling(MoveColor);
1209  if (thisRook === null) { return false; }
1210
1211  // check no piece between king and rook
1212  // clear king/rook squares for Chess960
1213  Board[PieceCol[MoveColor][0]][MoveColor*7] = 0;
1214  Board[PieceCol[MoveColor][thisRook]][MoveColor*7] = 0;
1215  var col = PieceCol[MoveColor][thisRook];
1216  if (col > 2) { col = 2; }
1217  while ((col < PieceCol[MoveColor][0]) || (col <= 3)) {
1218   if (Board[col][MoveColor*7] !== 0) { return false; }
1219   ++col;
1220  }
1221  castleRook = thisRook;
1222  return true;
1223}
1224
1225function CheckClearWay(thisPiece) {
1226  var stepCol = sign(mvToCol-PieceCol[MoveColor][thisPiece]);
1227  var stepRow = sign(mvToRow-PieceRow[MoveColor][thisPiece]);
1228  var startCol = PieceCol[MoveColor][thisPiece]+stepCol;
1229  var startRow = PieceRow[MoveColor][thisPiece]+stepRow;
1230  while ((startCol != mvToCol) || (startRow != mvToRow)) {
1231    if (Board[startCol][startRow] !== 0) { return false; }
1232    startCol += stepCol;
1233    startRow += stepRow;
1234  }
1235  return true;
1236}
1237
1238function CleanMove(move) {
1239  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
1240  if (move.match(/^[Oo0]/)) { move = move.replace(/[o0]/g, 'O').replace(/O(?=O)/g, 'O-'); }
1241  move = move.replace(/ep/i, '');
1242  return move;
1243}
1244
1245function GoToMove(thisPly, thisVar) {
1246  SetAutoPlay(false);
1247  if (typeof(thisVar) == "undefined") { thisVar = CurrentVar; }
1248  else {
1249    if (thisVar < 0) { thisVar = 0; }
1250    else if (thisVar >= numberOfVars) { thisVar = numberOfVars - 1; }
1251  }
1252  if (thisPly < 0) { thisPly = 0; }
1253  else if (thisPly >= StartPlyVar[thisVar] + PlyNumberVar[thisVar]) {
1254    thisPly = StartPlyVar[thisVar] + PlyNumberVar[thisVar];
1255  }
1256
1257  if (thisVar === CurrentVar) {
1258    var diff = thisPly - CurrentPly;
1259    if (diff > 0) { MoveForward(diff); }
1260    else { MoveBackward(-diff); }
1261  } else {
1262    var backStart = StartPly;
1263loopCommonPredecessor:
1264    for (var ii = PredecessorsVars[CurrentVar].length - 1; ii >= 0; ii--) {
1265      for (var jj = PredecessorsVars[thisVar].length - 1; jj >= 0; jj--) {
1266        if (PredecessorsVars[CurrentVar][ii] === PredecessorsVars[thisVar][jj]) {
1267          backStart = Math.min(PredecessorsVars[CurrentVar][ii+1] ? StartPlyVar[PredecessorsVars[CurrentVar][ii+1]] : CurrentPly, PredecessorsVars[thisVar][jj+1] ? StartPlyVar[PredecessorsVars[thisVar][jj+1]] : thisPly);
1268          break loopCommonPredecessor;
1269        }
1270      }
1271    }
1272    MoveBackward(CurrentPly - backStart, true);
1273    MoveForward(thisPly - backStart, thisVar);
1274  }
1275}
1276
1277
1278function SetShortcutKeysEnabled(onOff) {
1279  shortcutKeysEnabled = onOff;
1280}
1281
1282function interactivelyToggleShortcutKeys() {
1283  if (confirm("Shortcut keys currently " + (shortcutKeysEnabled ? "enabled" : "disabled") + ".\nToggle shortcut keys to " + (shortcutKeysEnabled ? "DISABLED" : "ENABLED") + "?")) {
1284    SetShortcutKeysEnabled(!shortcutKeysEnabled);
1285  }
1286}
1287
1288function SetCommentsIntoMoveText(onOff) {
1289  commentsIntoMoveText = onOff;
1290}
1291
1292function SetCommentsOnSeparateLines(onOff) {
1293  commentsOnSeparateLines = onOff;
1294}
1295
1296function SetAutostartAutoplay(onOff) {
1297  autostartAutoplay = onOff;
1298}
1299
1300function SetAutoplayNextGame(onOff) {
1301  autoplayNextGame = onOff;
1302}
1303
1304function SetInitialHalfmove(number_or_string, always) {
1305  alwaysInitialHalfmove = (always === true);
1306  initialHalfmove = typeof(number_or_string) == "undefined" ? 0 : number_or_string;
1307}
1308
1309function SetInitialVariation(number) {
1310  initialVariation = isNaN(number = parseInt(number, 10)) ? 0 : number;
1311}
1312
1313function SetInitialGame(number_or_string) {
1314  initialGame = typeof(number_or_string) == "undefined" ? 1 : number_or_string;
1315}
1316
1317function randomGameRandomPly() {
1318  if (numberOfGames > 1) {
1319    var oldInitialHalfmove = initialHalfmove;
1320    var oldAlwaysInitialHalfmove = alwaysInitialHalfmove;
1321    SetInitialHalfmove("random", true);
1322    Init(Math.floor(Math.random()*numberOfGames));
1323    SetInitialHalfmove(oldInitialHalfmove, oldAlwaysInitialHalfmove);
1324  }
1325}
1326
1327
1328// clock detection as [%clk 01:02]
1329
1330function clockFromComment(plyNum) {
1331  return customPgnCommentTag("clk", null, plyNum);
1332}
1333
1334function clockFromHeader(whiteToMove) {
1335  var clockString = customPgnHeaderTag("Clock") + "";
1336  var matches = clockString.match("^" + (whiteToMove ? "W" : "B") + "/(.*)$");
1337  if (matches) { return matches[1]; }
1338  else { return null; }
1339}
1340
1341function HighlightLastMove() {
1342  var theObj, moveId, text, ii, clockString, clockRegExp, clockMatch;
1343
1344  undoStackStore();
1345
1346  // remove old move highlighting
1347  if (highlightedMoveId) {
1348    if (theObj = document.getElementById(highlightedMoveId)) {
1349      theObj.className = (highlightedMoveId.match(/Var0Mv/) ? 'move' : 'variation') + ' notranslate';
1350    }
1351  }
1352
1353  // halfmove to be highlighted, negative for starting position
1354  var showThisMove = CurrentPly - 1;
1355  if (showThisMove > StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]) { showThisMove = StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]; }
1356
1357  if (theObj = document.getElementById("GameLastComment")) {
1358    if (commentsIntoMoveText) {
1359      variationTextDepth = CurrentVar === 0 ? 0 : 1;
1360      text = '<SPAN CLASS="comment">' + strippedMoveComment(showThisMove+1, CurrentVar, true).replace(/\sID="[^"]*"/g, '') + '</SPAN>';
1361    } else { text = ''; }
1362    theObj.innerHTML = text;
1363  }
1364
1365  // side to move
1366  var whiteToMove = ((showThisMove+1)%2 === 0);
1367  text = whiteToMove ? 'white' : 'black';
1368
1369  if (theObj = document.getElementById("GameSideToMove")) { theObj.innerHTML = text; }
1370
1371  // clock
1372  var lastMoverClockObj = document.getElementById(whiteToMove ? "GameBlackClock" : "GameWhiteClock");
1373  var initialLastMoverClock = whiteToMove ? gameInitialBlackClock[currentGame] : gameInitialWhiteClock[currentGame];
1374  var beforeLastMoverClockObj = document.getElementById(whiteToMove ? "GameWhiteClock" : "GameBlackClock");
1375  var initialBeforeLastMoverClock = whiteToMove ? gameInitialWhiteClock[currentGame] : gameInitialBlackClock[currentGame];
1376
1377  if (lastMoverClockObj) {
1378    clockString = ((showThisMove+1 === StartPly+PlyNumber) && ((!LiveBroadcastDemo) || (gameResult[currentGame] !== "*"))) ? clockFromHeader(!whiteToMove) : null;
1379    if (clockString === null) {
1380      clockString = showThisMove+1 > StartPly ? clockFromComment(showThisMove+1) : initialLastMoverClock;
1381      if (!clockString && (CurrentPly === StartPly+PlyNumber)) {
1382        // support for time info in the last comment as { White Time: 0h:12min Black Time: 1h:23min }
1383        clockRegExp = new RegExp((whiteToMove ? "Black" : "White") + "\\s+Time:\\s*(\\S+)", "i");
1384        if (clockMatch = strippedMoveComment(StartPly+PlyNumber).match(clockRegExp)) {
1385          clockString = clockMatch[1];
1386        }
1387      }
1388    }
1389    lastMoverClockObj.innerHTML = clockString;
1390  }
1391  if (beforeLastMoverClockObj) {
1392    clockString = ((showThisMove+1 === StartPly+PlyNumber) && ((!LiveBroadcastDemo) || (gameResult[currentGame] !== "*"))) ? clockFromHeader(whiteToMove) : null;
1393    if (clockString === null) {
1394      clockString = showThisMove > StartPly ? clockFromComment(showThisMove) : initialBeforeLastMoverClock;
1395      if (!clockString && (CurrentPly === StartPly+PlyNumber)) {
1396        // see comment above
1397        clockRegExp = new RegExp((whiteToMove ? "White" : "Black") + "\\s+Time:\\s*(\\S+)", "i");
1398        if (clockMatch = strippedMoveComment(StartPly+PlyNumber).match(clockRegExp)) {
1399          clockString = clockMatch[1];
1400        }
1401      }
1402    }
1403    beforeLastMoverClockObj.innerHTML = clockString;
1404  }
1405
1406  if (lastMoverClockObj && beforeLastMoverClockObj) {
1407    if (lastMoverClockObj.innerHTML && !beforeLastMoverClockObj.innerHTML) {
1408      beforeLastMoverClockObj.innerHTML = "-";
1409    } else if (!lastMoverClockObj.innerHTML && beforeLastMoverClockObj.innerHTML) {
1410      lastMoverClockObj.innerHTML = "-";
1411    }
1412  }
1413
1414  // next move
1415  if (theObj = document.getElementById("GameNextMove")) {
1416    if (CurrentVar === 0 && showThisMove + 1 >= StartPly + PlyNumber) {
1417      text = '<SPAN CLASS="move notranslate">' + gameResult[currentGame] + '</SPAN>';
1418    } else if (typeof(Moves[showThisMove+1]) == "undefined") {
1419      text = "";
1420    } else {
1421      text = printMoveText(showThisMove+1, CurrentVar, (CurrentVar !== 0), true, false);
1422    }
1423    theObj.innerHTML = text;
1424  }
1425
1426  // next variations
1427  if (theObj = document.getElementById("GameNextVariations")) {
1428    text = '';
1429    if (commentsIntoMoveText) {
1430      var children = childrenVars(showThisMove+1, CurrentVar);
1431      for (ii = 0; ii < children.length; ii++) {
1432        if (children[ii] !== CurrentVar) {
1433          text += ' ' + printMoveText(showThisMove+1, children[ii], (children[ii] !== 0), true, false);
1434        }
1435      }
1436    }
1437    theObj.innerHTML = text;
1438  }
1439
1440  // last move
1441  if (theObj = document.getElementById("GameLastMove")) {
1442    if ((showThisMove >= StartPly) && Moves[showThisMove]) {
1443      text = printMoveText(showThisMove, CurrentVar, (CurrentVar !== 0), true, false);
1444    } else if (showThisMove === StartPly - 1) {
1445      text = '<SPAN CLASS="move notranslate">' + (Math.floor((showThisMove+1)/2) + 1) + (((showThisMove+1) % 2) ? "..." : ".") + '</SPAN>';
1446    } else { text = ''; }
1447    theObj.innerHTML = text;
1448  }
1449
1450  // last variations
1451  if (theObj = document.getElementById("GameLastVariations")) {
1452    text = '';
1453    if (commentsIntoMoveText) {
1454      var siblings = childrenVars(showThisMove, HistVar[showThisMove]);
1455      for (ii = 0; ii < siblings.length; ii++) {
1456        if (siblings[ii] !== CurrentVar) {
1457          text += ' ' + printMoveText(showThisMove, siblings[ii], (siblings[ii] !== 0), true, false);
1458        }
1459      }
1460    }
1461    theObj.innerHTML = text;
1462  }
1463
1464  if (showThisMove >= (StartPlyVar[CurrentVar]-1)) {
1465    moveId = 'Var' + CurrentVar + 'Mv' + (showThisMove + 1);
1466    if (theObj = document.getElementById(moveId)) {
1467      theObj.className = (CurrentVar ? 'variation variationOn' : 'move moveOn') + ' notranslate';
1468    }
1469    highlightedMoveId = moveId;
1470
1471    if (highlightOption) {
1472      var colFrom, rowFrom, colTo, rowTo;
1473      if ((showThisMove < StartPly) || HistNull[showThisMove]) {
1474        colFrom = rowFrom = -1;
1475        colTo   = rowTo   = -1;
1476      } else {
1477        colFrom = HistCol[0][showThisMove] === undefined ? -1 : HistCol[0][showThisMove];
1478        rowFrom = HistRow[0][showThisMove] === undefined ? -1 : HistRow[0][showThisMove];
1479        colTo   = HistCol[2][showThisMove] === undefined ? -1 : HistCol[2][showThisMove];
1480        rowTo   = HistRow[2][showThisMove] === undefined ? -1 : HistRow[2][showThisMove];
1481      }
1482      highlightMove(colFrom, rowFrom, colTo, rowTo);
1483    }
1484  }
1485}
1486
1487function SetHighlightOption(on) {
1488  highlightOption = on;
1489}
1490
1491function SetHighlight(on) {
1492  SetHighlightOption(on);
1493  if (on) { HighlightLastMove(); }
1494  else { highlightMove(-1, -1, -1, -1); }
1495}
1496
1497var colFromHighlighted = -1;
1498var rowFromHighlighted = -1;
1499var colToHighlighted = -1;
1500var rowToHighlighted = -1;
1501function highlightMove(colFrom, rowFrom, colTo, rowTo) {
1502  highlightSquare(colFromHighlighted, rowFromHighlighted, false);
1503  highlightSquare(colToHighlighted, rowToHighlighted, false);
1504  if ( highlightSquare(colFrom, rowFrom, true) ) {
1505    colFromHighlighted = colFrom;
1506    rowFromHighlighted = rowFrom;
1507  } else { colFromHighlighted = rowFromHighlighted = -1; }
1508  if ( highlightSquare(colTo, rowTo, true) ) {
1509    colToHighlighted = colTo;
1510    rowToHighlighted = rowTo;
1511  } else { colToHighlighted = rowToHighlighted = -1; }
1512}
1513
1514function highlightSquare(col, row, on) {
1515  if ((col === undefined) || (row === undefined)) { return false; }
1516  if (!SquareOnBoard(col, row)) { return false; }
1517  var trow = IsRotated ? row : 7 - row;
1518  var tcol = IsRotated ? 7 - col : col;
1519  var theObj = document.getElementById('tcol' + tcol + 'trow' + trow);
1520  if (!theObj) { return false; }
1521  if (on) { theObj.className = (trow+tcol)%2 === 0 ? "highlightWhiteSquare" : "highlightBlackSquare"; }
1522  else { theObj.className = (trow+tcol)%2 === 0 ? "whiteSquare" : "blackSquare"; }
1523  return true;
1524}
1525
1526var undoStackMax = 1000;
1527var undoStackGame = new Array(undoStackMax);
1528var undoStackVar = new Array(undoStackMax);
1529var undoStackPly = new Array(undoStackMax);
1530var undoStackStart = 0;
1531var undoStackCurrent = 0;
1532var undoStackEnd = 0;
1533var undoRedoInProgress = false;
1534
1535function undoStackReset() {
1536  undoStackGame = new Array(undoStackMax);
1537  undoStackVar = new Array(undoStackMax);
1538  undoStackPly = new Array(undoStackMax);
1539  undoStackStart = undoStackCurrent = undoStackEnd = 0;
1540}
1541
1542function undoStackStore() {
1543  if (undoRedoInProgress) { return false; }
1544  if ((undoStackStart === undoStackCurrent) || (currentGame !== undoStackGame[undoStackCurrent]) || (CurrentVar !== undoStackVar[undoStackCurrent]) || (CurrentPly !== undoStackPly[undoStackCurrent])) {
1545    undoStackCurrent = (undoStackCurrent + 1) % undoStackMax;
1546    undoStackGame[undoStackCurrent] = currentGame;
1547    undoStackVar[undoStackCurrent] = CurrentVar;
1548    undoStackPly[undoStackCurrent] = CurrentPly;
1549    undoStackEnd = undoStackCurrent;
1550    if (undoStackStart === undoStackCurrent) { undoStackStart = (undoStackStart + 1) % undoStackMax; }
1551  }
1552  return true;
1553}
1554
1555function undoStackUndo() {
1556  if ((undoStackCurrent - 1 + undoStackMax) % undoStackMax === undoStackStart) { return false; }
1557  undoRedoInProgress = true;
1558  undoStackCurrent = (undoStackCurrent - 1 + undoStackMax) % undoStackMax;
1559  if (undoStackGame[undoStackCurrent] !== currentGame) { Init(undoStackGame[undoStackCurrent]); }
1560  GoToMove(undoStackPly[undoStackCurrent], undoStackVar[undoStackCurrent]);
1561  undoRedoInProgress = false;
1562  return true;
1563}
1564
1565function undoStackRedo() {
1566  if (undoStackCurrent === undoStackEnd) { return false; }
1567  undoRedoInProgress = true;
1568  undoStackCurrent = (undoStackCurrent + 1) % undoStackMax;
1569  if (undoStackGame[undoStackCurrent] !== currentGame) { Init(undoStackGame[undoStackCurrent]); }
1570  GoToMove(undoStackPly[undoStackCurrent], undoStackVar[undoStackCurrent]);
1571  undoRedoInProgress = false;
1572  return true;
1573}
1574
1575
1576function fixCommonPgnMistakes(text) {
1577  text = text.replace(/[\u00A0\u180E\u2000-\u200A\u202F\u205F\u3000]/g," "); // some spaces to plain space
1578  text = text.replace(/\u00BD/g,"1/2"); // "half fraction" to "1/2"
1579  text = text.replace(/[\u2010-\u2015]/g,"-"); // "hyphens" to "-"
1580  text = text.replace(/\u2024/g,"."); // "one dot leader" to "."
1581  text = text.replace(/[\u2025-\u2026]/g,"..."); // "two dot leader" and "ellipsis" to "..."
1582  text = text.replace(/\\"/g,"'"); // fix [Opening "Queen\"s Gambit"]
1583  return text;
1584}
1585
1586function fullPgnGame(gameNum) {
1587  var res = pgnHeader[gameNum] ? pgnHeader[gameNum].replace(/^[^[]*/g, "") : "";
1588  res = res.replace(/\[\s*(\w+)\s*"([^"]*)"\s*\][^[]*/g, '[$1 "$2"]\n');
1589  res += "\n";
1590  res += pgnGame[gameNum] ? pgnGame[gameNum].replace(/(^[\s]*|[\s]*$)/g, "") : "";
1591  return res;
1592}
1593
1594function pgnGameFromPgnText(pgnText) {
1595
1596  var newNumGames, headMatch, prevHead, newHead, startNew, afterNew, lastOpen, checkedGame, validHead;
1597
1598  pgnText = simpleHtmlentities(fixCommonPgnMistakes(pgnText));
1599
1600  // PGN standard: ignore lines starting with %
1601  pgnText = pgnText.replace(/(^|\n)%.*(\n|$)/g, "\n");
1602
1603  newNumGames = 0;
1604  checkedGame = "";
1605  while (headMatch = pgnHeaderBlockRegExp.exec(pgnText)) {
1606    newHead = headMatch[0];
1607    startNew = pgnText.indexOf(newHead);
1608    afterNew = startNew + newHead.length;
1609    if (prevHead) {
1610      checkedGame += pgnText.slice(0, startNew);
1611      validHead = ((lastOpen = checkedGame.lastIndexOf("{")) < 0) || (checkedGame.lastIndexOf("}")) > lastOpen;
1612      if (validHead) {
1613        pgnHeader[newNumGames] = prevHead;
1614        pgnGame[newNumGames++] = checkedGame;
1615        checkedGame = "";
1616      } else {
1617        checkedGame += newHead;
1618      }
1619    } else {
1620      validHead = true;
1621    }
1622    if (validHead) { prevHead = newHead; }
1623    pgnText = pgnText.slice(afterNew);
1624  }
1625  if (prevHead) {
1626    pgnHeader[newNumGames] = prevHead;
1627    checkedGame += pgnText;
1628    pgnGame[newNumGames++] = checkedGame;
1629  }
1630
1631  if (newNumGames === 0) { return false; }
1632  numberOfGames = newNumGames;
1633  return true;
1634}
1635
1636
1637function pgnGameFromHttpRequest(httpResponseData) {
1638
1639  // process here any special file types, for instance zipfiles:
1640  //   if (pgnUrl && pgnUrl.replace(/[?#].*/, "").match(/\.zip$/i)) { return pgnGameFromPgnText(unzipPgnFiles(httpResponseData)); }
1641  // remember to fix function loadPgnFromPgnUrl() for binary data
1642
1643  return pgnGameFromPgnText(httpResponseData);
1644}
1645
1646var http_request_last_processed_id = 0;
1647function updatePgnFromHttpRequest(this_http_request, this_http_request_id) {
1648  var res = LOAD_PGN_FAIL;
1649
1650  if (this_http_request.readyState != 4) { return; }
1651
1652  if (this_http_request_id < http_request_last_processed_id) { return; }
1653  else { http_request_last_processed_id = this_http_request_id; }
1654
1655  if ((this_http_request.status == 200) || (this_http_request.status == 304)) {
1656
1657    if (this_http_request.status == 304) {
1658      if (LiveBroadcastDelay > 0) {
1659        res = LOAD_PGN_UNMODIFIED;
1660      } else {
1661        myAlert('error: unmodified PGN URL when not in live mode');
1662      }
1663    } else if (!this_http_request.responseText) {
1664      myAlert('error: no data received from PGN URL\n' + pgnUrl, true);
1665    } else if (!pgnGameFromHttpRequest(this_http_request.responseText)) {
1666      myAlert('error: no games found at PGN URL\n' + pgnUrl, true);
1667    } else {
1668      if (LiveBroadcastDelay > 0) {
1669        LiveBroadcastLastReceivedLocal = (new Date()).toLocaleString();
1670        if (LiveBroadcastLastModifiedHeader = this_http_request.getResponseHeader("Last-Modified")) {
1671          LiveBroadcastLastModified = new Date(LiveBroadcastLastModifiedHeader);
1672        } else { LiveBroadcastLastModified_Reset(); }
1673      }
1674      res = LOAD_PGN_OK;
1675    }
1676
1677  } else {
1678    myAlert('error: failed reading PGN URL\n' + pgnUrl, true);
1679  }
1680
1681  if (LiveBroadcastDemo && (res == LOAD_PGN_UNMODIFIED)) {
1682    res = LOAD_PGN_OK;
1683  }
1684
1685  loadPgnCheckingLiveStatus(res);
1686}
1687
1688var LOAD_PGN_FAIL = 0;
1689var LOAD_PGN_OK = 1;
1690var LOAD_PGN_UNMODIFIED = 2;
1691function loadPgnCheckingLiveStatus(res) {
1692
1693  switch (res) {
1694
1695    case LOAD_PGN_OK:
1696      if (LiveBroadcastDelay > 0) {
1697        firstStart = true;
1698        var oldParseLastMoveError = ParseLastMoveError;
1699        if (!LiveBroadcastStarted) {
1700          LiveBroadcastStarted = true;
1701        } else {
1702          var oldWhite = gameWhite[currentGame];
1703          var oldBlack = gameBlack[currentGame];
1704          var oldEvent = gameEvent[currentGame];
1705          var oldRound = gameRound[currentGame];
1706          var oldSite  = gameSite[currentGame];
1707          var oldDate  = gameDate[currentGame];
1708
1709          initialGame = currentGame + 1;
1710
1711          LiveBroadcastOldCurrentVar = CurrentVar;
1712          LiveBroadcastOldCurrentPly = CurrentPly;
1713          LiveBroadcastOldCurrentPlyLast = (CurrentVar === 0 && CurrentPly === StartPlyVar[0] + PlyNumberVar[0]);
1714
1715          var oldAutoplay = isAutoPlayOn;
1716          if (isAutoPlayOn) { SetAutoPlay(false); }
1717
1718          LoadGameHeaders();
1719          LiveBroadcastFoundOldGame = false;
1720          for (var ii=0; ii<numberOfGames; ii++) {
1721            LiveBroadcastFoundOldGame = (gameWhite[ii]==oldWhite) && (gameBlack[ii]==oldBlack) && (gameEvent[ii]==oldEvent) && (gameRound[ii]==oldRound) && (gameSite[ii] ==oldSite ); // && (gameDate[ii] ==oldDate );
1722            if (LiveBroadcastFoundOldGame) { break; }
1723          }
1724          if (LiveBroadcastFoundOldGame) { initialGame = ii + 1; }
1725
1726          if (LiveBroadcastFoundOldGame) {
1727            var oldInitialVariation = initialVariation;
1728            var oldInitialHalfmove = initialHalfmove;
1729            initialVariation = CurrentVar;
1730            if (LiveBroadcastSteppingMode) {
1731              initialHalfmove = (LiveBroadcastOldCurrentPlyLast || oldParseLastMoveError) ? LiveBroadcastOldCurrentPly+1 : LiveBroadcastOldCurrentPly;
1732            } else {
1733              initialHalfmove = (LiveBroadcastOldCurrentPlyLast || oldParseLastMoveError) ? "end" : LiveBroadcastOldCurrentPly;
1734            }
1735          }
1736        }
1737      }
1738
1739      undoStackReset();
1740      Init();
1741
1742      if (LiveBroadcastDelay > 0) {
1743        if (LiveBroadcastFoundOldGame) {
1744          initialHalfmove = oldInitialHalfmove;
1745          initialVariation = oldInitialVariation;
1746        }
1747        checkLiveBroadcastStatus();
1748      }
1749
1750      customFunctionOnPgnTextLoad();
1751
1752      if (LiveBroadcastDelay > 0) {
1753        if (LiveBroadcastFoundOldGame) {
1754          if (LiveBroadcastSteppingMode) {
1755            if (oldAutoplay || LiveBroadcastOldCurrentPlyLast || oldParseLastMoveError) { SetAutoPlay(true); }
1756          } else {
1757            if (oldAutoplay) { SetAutoPlay(true); }
1758          }
1759        }
1760      }
1761
1762      break;
1763
1764    case LOAD_PGN_UNMODIFIED:
1765      if (LiveBroadcastDelay > 0) {
1766        checkLiveBroadcastStatus();
1767      }
1768      break;
1769
1770    case LOAD_PGN_FAIL:
1771    default:
1772      if (LiveBroadcastDelay === 0) {
1773        pgnGameFromPgnText(alertPgn);
1774        undoStackReset();
1775        Init();
1776        customFunctionOnPgnTextLoad();
1777      } else { // live broadcast: wait for live show start
1778        if (!LiveBroadcastStarted) {
1779          pgnGameFromPgnText(LiveBroadcastPlaceholderPgn);
1780          firstStart = true;
1781          undoStackReset();
1782          Init();
1783          checkLiveBroadcastStatus();
1784          customFunctionOnPgnTextLoad();
1785        } else { checkLiveBroadcastStatus(); }
1786      }
1787      break;
1788
1789  }
1790
1791  if (LiveBroadcastDelay > 0) { restartLiveBroadcastTimeout(); }
1792}
1793
1794var http_request_last_id = 0;
1795function loadPgnFromPgnUrl(pgnUrl) {
1796
1797  LiveBroadcastLastRefreshedLocal = (new Date()).toLocaleString();
1798
1799  try {
1800    var http_request = new XMLHttpRequest();
1801    if (http_request.overrideMimeType) {
1802      // 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
1803      http_request.overrideMimeType("text/plain"); // this assumes the PGN file is encoded as unicode UTF-8
1804      // 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
1805      // http_request.overrideMimeType("text/plain; charset=x-user-defined"); // this works with a binary file, such as a zipfile, but would need further processing
1806    }
1807  } catch(e) {
1808    myAlert('error: failed creating XMLHttpRequest for PGN URL\n' + pgnUrl, true);
1809    loadPgnCheckingLiveStatus(LOAD_PGN_FAIL);
1810    return false;
1811  }
1812
1813  var http_request_id = http_request_last_id++;
1814  http_request.onreadystatechange = function () { updatePgnFromHttpRequest(http_request, http_request_id); };
1815
1816  try {
1817    var randomizer = "";
1818    // anti-caching #1
1819    if ((LiveBroadcastDelay > 0) && (pgnUrl.indexOf("?") == -1) && (pgnUrl.indexOf("#") == -1)) {
1820      randomizer = "?noCache=" + (0x1000000000 + Math.floor((Math.random() * 0xF000000000))).toString(16).toUpperCase();
1821    }
1822    http_request.open("GET", pgnUrl + randomizer);
1823    // anti-caching #2
1824    if (LiveBroadcastDelay > 0) {
1825      http_request.setRequestHeader( "If-Modified-Since", LiveBroadcastLastModifiedHeader );
1826    }
1827    http_request.send(null);
1828  } catch(e) {
1829    myAlert('error: failed sending XMLHttpRequest for PGN URL\n' + pgnUrl, true);
1830    return false;
1831  }
1832
1833  return true;
1834}
1835
1836function SetPgnUrl(url) {
1837  pgnUrl = url;
1838}
1839
1840
1841function LiveBroadcastLastModified_Reset() {
1842  LiveBroadcastLastModified = new Date(0);
1843  LiveBroadcastLastModifiedHeader = LiveBroadcastLastModified.toUTCString();
1844}
1845
1846function LiveBroadcastLastReceivedLocal_Reset() {
1847  LiveBroadcastLastReceivedLocal = 'unavailable';
1848}
1849
1850function LiveBroadcastLastModified_ServerTime() {
1851  return LiveBroadcastLastModified.getTime() === 0 ? 'unavailable' : LiveBroadcastLastModifiedHeader;
1852}
1853
1854function pauseLiveBroadcast() {
1855  if ((LiveBroadcastDelay === 0) || (LiveBroadcastPaused)) { return; }
1856  LiveBroadcastPaused = true;
1857  clearTimeout(LiveBroadcastInterval);
1858  LiveBroadcastInterval = null;
1859  LiveBroadcastTicker--;
1860  checkLiveBroadcastStatus();
1861  LiveBroadcastTicker++;
1862}
1863
1864function restartLiveBroadcast() {
1865  if (LiveBroadcastDelay === 0) { return; }
1866  LiveBroadcastPaused = false;
1867  refreshPgnSource();
1868}
1869
1870function checkLiveBroadcastStatus() {
1871  if (LiveBroadcastDelay === 0) { return; }
1872
1873  var theTitle, theHTML, theObj, ii;
1874
1875  // broadcast started yet?
1876  if (LiveBroadcastStarted === false || typeof(pgnHeader) == "undefined" || (numberOfGames == 1 && gameEvent[0] == LiveBroadcastPlaceholderEvent)) {
1877    // no
1878    LiveBroadcastEnded = false;
1879    LiveBroadcastGamesRunning = 0;
1880    theTitle = "live broadcast yet to start";
1881  } else {
1882    // yes
1883    var lbgr = 0, lbga = 0;
1884    for (ii=0; ii<numberOfGames; ii++) { if (gameResult[ii].indexOf('*') >= 0) { lbga++; if (!pgnGame[ii].match(/^\s*\*?\s*$/)) { lbgr++; } } }
1885    LiveBroadcastEnded = ((lbga === 0) && (!LiveBroadcastEndlessMode));
1886    LiveBroadcastGamesRunning = lbgr;
1887    theTitle = LiveBroadcastEnded ? "live broadcast ended" : LiveBroadcastPaused ? "live broadcast paused" : lbgr + " live game" + (lbgr == 1 ? "" : "s") + " out of " + numberOfGames;
1888  }
1889  theHTML = LiveBroadcastEnded ? "#" : LiveBroadcastPaused ? "+" : "=";
1890  theHTML = (LiveBroadcastTicker % 4 === 0 ? theHTML : "&nbsp;") + (LiveBroadcastTicker % 2 === 1 ? theHTML : "&nbsp;") + (LiveBroadcastTicker % 4 === 2 ? theHTML : "&nbsp;");
1891  theHTML = LiveBroadcastGamesRunning + "<span style='display:inline-block; min-width:3em; text-align:center;'>" + theHTML + "</span>" + numberOfGames;
1892  theHTML = "<span onclick='" + (LiveBroadcastPaused ? "restartLiveBroadcast();" : "refreshPgnSource();") + " this.blur();'>" + theHTML + "</span>";
1893
1894  if (theObj = document.getElementById("GameLiveStatus")) {
1895    theObj.innerHTML = theHTML;
1896    theObj.title = theTitle;
1897  }
1898
1899  if (theObj = document.getElementById("GameLiveLastRefreshed")) { theObj.innerHTML = LiveBroadcastLastRefreshedLocal; }
1900  if (theObj = document.getElementById("GameLiveLastReceived")) { theObj.innerHTML = LiveBroadcastLastReceivedLocal; }
1901  if (theObj = document.getElementById("GameLiveLastModifiedServer")) { theObj.innerHTML = LiveBroadcastLastModified_ServerTime(); }
1902
1903  customFunctionOnCheckLiveBroadcastStatus();
1904}
1905
1906function restartLiveBroadcastTimeout() {
1907  if (LiveBroadcastDelay === 0) { return; }
1908  if (LiveBroadcastInterval) { clearTimeout(LiveBroadcastInterval); LiveBroadcastInterval = null; }
1909  if ((!LiveBroadcastEnded) && (!LiveBroadcastPaused)) {
1910    LiveBroadcastInterval = setTimeout("refreshPgnSource()", LiveBroadcastDelay * 60000);
1911  }
1912  LiveBroadcastTicker++;
1913}
1914
1915var LiveBroadcastFoundOldGame = false;
1916var LiveBroadcastOldCurrentVar;
1917var LiveBroadcastOldCurrentPly;
1918var LiveBroadcastOldCurrentPlyLast = false;
1919function refreshPgnSource() {
1920  if (LiveBroadcastDelay === 0) { return; }
1921  if (LiveBroadcastInterval) { clearTimeout(LiveBroadcastInterval); LiveBroadcastInterval = null; }
1922  if (LiveBroadcastDemo) {
1923    var newPly, addedPly = 0;
1924    for (var ii=0; ii<numberOfGames; ii++) {
1925      //        5% 15%      40%
1926      newPly = [3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1][Math.floor(20 * Math.random())] || 0;
1927      if (gameDemoMaxPly[ii] <= gameDemoLength[ii]) {
1928        gameDemoMaxPly[ii] += newPly;
1929        addedPly += newPly;
1930      }
1931    }
1932    if (addedPly > 0) { LiveBroadcastLastReceivedLocal = (new Date()).toLocaleString(); }
1933  }
1934
1935  if (pgnUrl) {
1936    loadPgnFromPgnUrl(pgnUrl);
1937  } else if ( document.getElementById("pgnText") ) {
1938    loadPgnFromTextarea("pgnText");
1939  } else {
1940    pgnGameFromPgnText(alertPgn);
1941    undoStackReset();
1942    Init();
1943    customFunctionOnPgnTextLoad();
1944    myAlert('error: missing PGN URL location and pgnText object in the HTML file', true);
1945  }
1946}
1947
1948function loadPgnFromTextarea(textareaId) {
1949  var res = LOAD_PGN_FAIL, text, theObj;
1950
1951  LiveBroadcastLastRefreshedLocal = (new Date()).toLocaleString();
1952
1953  if (!(theObj = document.getElementById(textareaId))) {
1954    myAlert('error: missing ' + textareaId + ' textarea object in the HTML file', true);
1955  } else {
1956    if (document.getElementById(textareaId).tagName.toLowerCase() == "textarea") {
1957      text = document.getElementById(textareaId).value;
1958    } else { // compatibility with pgn4web up to 1.77: <span> used for pgnText
1959      text = document.getElementById(textareaId).innerHTML;
1960      // fixes browser issue removing \n from innerHTML
1961      if (text.indexOf('\n') < 0) { text = text.replace(/((\[[^\[\]]*\]\s*)+)/g, "\n$1\n"); }
1962      // fixes browser issue replacing quotes with &quot;
1963      if (text.indexOf('"') < 0) { text = text.replace(/(&quot;)/g, '"'); }
1964    }
1965
1966    // no header: add emptyPgnHeader
1967    if (pgnHeaderTagRegExp.test(text) === false) { text = emptyPgnHeader + "\n" + text; }
1968
1969    if ( pgnGameFromPgnText(text) ) {
1970      res = LOAD_PGN_OK;
1971      LiveBroadcastLastReceivedLocal = (new Date()).toLocaleString();
1972    } else {
1973      myAlert('error: no games found in ' + textareaId + ' object in the HTML file');
1974    }
1975  }
1976
1977  loadPgnCheckingLiveStatus(res);
1978}
1979
1980function createBoard() {
1981  if (pgnUrl) {
1982    loadPgnFromPgnUrl(pgnUrl);
1983  } else if ( document.getElementById("pgnText") ) {
1984    loadPgnFromTextarea("pgnText");
1985  } else {
1986    pgnGameFromPgnText(alertPgn);
1987    undoStackReset();
1988    Init();
1989    customFunctionOnPgnTextLoad();
1990    myAlert('error: missing PGN URL location or pgnText in the HTML file', true);
1991  }
1992}
1993
1994function setCurrentGameFromInitialGame() {
1995  switch (initialGame) {
1996    case "first":
1997      currentGame = 0;
1998      break;
1999    case "last":
2000      currentGame = numberOfGames - 1;
2001      break;
2002    case "random":
2003      currentGame = Math.floor(Math.random()*numberOfGames);
2004      break;
2005    default:
2006      if (isNaN(parseInt(initialGame,10))) {
2007        currentGame = gameNumberSearchPgn(initialGame, false, true);
2008        if (!currentGame) { currentGame = 0; }
2009      } else {
2010        initialGame = parseInt(initialGame,10);
2011        initialGame = initialGame < 0 ? -Math.floor(-initialGame) : Math.floor(initialGame);
2012        if (initialGame < -numberOfGames) { currentGame = 0; }
2013        else if (initialGame < 0) { currentGame = numberOfGames + initialGame; }
2014        else if (initialGame === 0) { currentGame = Math.floor(Math.random()*numberOfGames); }
2015        else if (initialGame <= numberOfGames) { currentGame = (initialGame - 1); }
2016        else { currentGame = numberOfGames - 1; }
2017      }
2018      break;
2019  }
2020}
2021
2022function GoToInitialHalfmove() {
2023  var iv, ih;
2024  if (initialVariation < 0) { iv = Math.max(numberOfVars + initialVariations, 0); }
2025  else { iv = Math.min(initialVariation, numberOfVars - 1); }
2026
2027  switch (initialHalfmove) {
2028    case "start":
2029      GoToMove((StartPlyVar[iv] + (iv ? 1 : 0)), iv);
2030      break;
2031    case "end":
2032      GoToMove(StartPlyVar[iv] + PlyNumberVar[iv], iv);
2033      break;
2034    case "random":
2035      GoToMove((StartPlyVar[iv] + (iv ? 1 : 0)) + Math.floor(Math.random()*(StartPlyVar[iv] + PlyNumberVar[iv])), iv);
2036      break;
2037    case "comment":
2038    case "variation":
2039      GoToMove((StartPlyVar[iv] + (iv ? 1 : 0)), iv);
2040      MoveToNextComment(initialHalfmove == "variation");
2041      break;
2042    default:
2043      if (isNaN(initialHalfmove = parseInt(initialHalfmove, 10))) { initialHalfmove = 0; }
2044      if (initialHalfmove < 0) { ih = Math.max(StartPlyVar[iv] + PlyNumberVar[iv] + 1 + initialHalfmove, StartPly); }
2045      else { ih = Math.min(initialHalfmove, StartPlyVar[iv] + PlyNumberVar[iv]); }
2046      GoToMove(ih, iv);
2047      break;
2048  }
2049}
2050
2051function Init(nextGame) {
2052
2053  if (nextGame !== undefined) {
2054    if ((!isNaN(nextGame)) && (nextGame >= 0) && (nextGame < numberOfGames)) {
2055      currentGame = parseInt(nextGame,10);
2056    } else { return; }
2057  }
2058
2059  if (isAutoPlayOn) { SetAutoPlay(false); }
2060
2061  InitImages();
2062  if (firstStart) {
2063    LoadGameHeaders();
2064    setCurrentGameFromInitialGame();
2065  }
2066
2067  if ((gameSetUp[currentGame] !== undefined) && (gameSetUp[currentGame] != "1")) { InitFEN(); }
2068  else { InitFEN(gameFEN[currentGame]); }
2069
2070  OpenGame(currentGame);
2071
2072  CurrentPly = StartPly;
2073  if (firstStart || alwaysInitialHalfmove) {
2074    GoToInitialHalfmove();
2075    setTimeout("autoScrollToCurrentMoveIfEnabled();", Math.min(666, 0.9 * Delay));
2076  } else {
2077    synchMoves();
2078    RefreshBoard();
2079    HighlightLastMove();
2080    autoScrollToCurrentMoveIfEnabled();
2081    // customFunctionOnMove here for consistency: null move starting new game
2082    customFunctionOnMove();
2083    if (typeof(engineWinOnMove) == "function") { engineWinOnMove(); }
2084  }
2085
2086  if ((firstStart) && (autostartAutoplay)) { SetAutoPlay(true); }
2087
2088  customFunctionOnPgnGameLoad();
2089
2090  initialVariation = 0;
2091  firstStart = false;
2092}
2093
2094function myAlertFEN(FenString, text) {
2095  myAlert("error: invalid FEN in game " + (currentGame+1) + ": " + text + "\n" + FenString, true);
2096}
2097
2098function InitFEN(startingFEN) {
2099  var ii, jj, cc, color, castlingRookCol, fullMoveNumber;
2100
2101  var FenString = typeof(startingFEN) != "string" ? FenStringStart : startingFEN.replace(/\\/g, "/").replace(/[^a-zA-Z0-9\s\/-]/g, " ").replace(/(^\s*|\s*$)/g, "").replace(/\s+/g, " ");
2102
2103  for (ii = 0; ii < 8; ++ii) {
2104    for (jj = 0; jj < 8; ++jj) {
2105      Board[ii][jj] = 0;
2106    }
2107  }
2108
2109  StartPly  = 0;
2110  MoveCount = StartPly;
2111  MoveColor = StartPly % 2;
2112
2113  var newEnPassant = false;
2114  var newEnPassantCol;
2115  CastlingLong = [0, 0];
2116  CastlingShort = [7, 7];
2117  InitialHalfMoveClock = 0;
2118
2119  HistVar[StartPly] = 0;
2120  HistNull[StartPly] = 0;
2121
2122  if (FenString == FenStringStart) {
2123    for (color = 0; color < 2; color++) {
2124      //                      K  Q  N     B     R     p
2125      PieceType[color]        = [1, 2, 5, 5, 4, 4, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6];
2126      PieceCol[color]         = [4, 3, 1, 6, 2, 5, 0, 7, 0, 1, 2, 3, 4, 5, 6, 7];
2127      PieceMoveCounter[color] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
2128      PieceRow[color] = color ? [7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6]:
2129                                [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1];
2130      for (ii = 0; ii < 16; ii++) {
2131        var col = PieceCol[color][ii];
2132        var row = PieceRow[color][ii];
2133        Board[col][row] = (1-2*color)*PieceType[color][ii];
2134      }
2135    }
2136  } else {
2137    var kk, ll, nn, mm;
2138    for (ii = 0; ii < 2; ii++) {
2139      for (jj = 0; jj < 16; jj++) {
2140        PieceType[ii][jj] = -1;
2141        PieceCol[ii][jj] = 0;
2142        PieceRow[ii][jj] = 0;
2143        PieceMoveCounter[ii][jj] = 0;
2144      }
2145    }
2146
2147    ii = 0; jj = 7; ll = 0; nn = 1; mm = 1; cc = FenString.charAt(ll++);
2148    while (cc != " ") {
2149      if (cc == "/") {
2150        if (ii != 8) {
2151          myAlertFEN(FenString, "char " + ll);
2152          InitFEN();
2153          return;
2154        }
2155        ii = 0;
2156        jj--;
2157      }
2158      if (ii == 8) {
2159        myAlertFEN(FenString, "char " + ll);
2160        InitFEN();
2161        return;
2162      }
2163      if (!isNaN(cc)) {
2164        ii += parseInt(cc,10);
2165        if ((ii < 0) || (ii > 8)) {
2166          myAlertFEN(FenString, "char " + ll);
2167          InitFEN();
2168          return;
2169        }
2170      }
2171      if (cc === PiecesArr[0].toUpperCase()) {
2172        if (PieceType[0][0] != -1) {
2173          myAlertFEN(FenString, "char " + ll);
2174          InitFEN();
2175          return;
2176        }
2177        PieceType[0][0] = 1;
2178        PieceCol[0][0] = ii;
2179        PieceRow[0][0] = jj;
2180        ii++;
2181      } else if (cc === PiecesArr[0].toLowerCase()) {
2182        if (PieceType[1][0] != -1) {
2183          myAlertFEN(FenString, "char " + ll);
2184          InitFEN();
2185          return;
2186        }
2187        PieceType[1][0] = 1;
2188        PieceCol[1][0] = ii;
2189        PieceRow[1][0] = jj;
2190        ii++;
2191      }
2192      for (kk = 1; kk < 6; kk++) {
2193        if (cc === PiecesArr[kk].toUpperCase()) {
2194          if (nn == 16) {
2195            myAlertFEN(FenString, "char " + ll);
2196            InitFEN();
2197            return;
2198          }
2199          PieceType[0][nn] = kk+1;
2200          PieceCol[0][nn] = ii;
2201          PieceRow[0][nn] = jj;
2202          nn++;
2203          ii++;
2204        } else if (cc === PiecesArr[kk].toLowerCase()) {
2205          if (mm==16) {
2206            myAlertFEN(FenString, "char " + ll);
2207            InitFEN();
2208            return;
2209          }
2210          PieceType[1][mm] = kk+1;
2211          PieceCol[1][mm] = ii;
2212          PieceRow[1][mm] = jj;
2213          mm++;
2214          ii++;
2215        }
2216      }
2217      cc = ll < FenString.length ? FenString.charAt(ll++) : " ";
2218    }
2219    if ((ii != 8) || (jj !== 0)) {
2220      myAlertFEN(FenString, "char " + ll);
2221      InitFEN();
2222      return;
2223    }
2224    if ((PieceType[0][0] == -1) || (PieceType[1][0] == -1)) {
2225      myAlertFEN(FenString, "missing King");
2226      InitFEN();
2227      return;
2228    }
2229    if (ll == FenString.length) {
2230      FenString += "w " + assumedCastleRights() + " - 0 1";
2231    }
2232    cc = FenString.charAt(ll++);
2233    if ((cc == "w") || (cc == "b")) {
2234      if (cc == "b") {
2235        StartPly += 1;
2236        MoveColor = 1;
2237      }
2238    } else {
2239      myAlertFEN(FenString, "invalid active color");
2240    }
2241
2242    // set board
2243    for (color = 0; color < 2; ++color) {
2244      for (ii = 0; ii < 16; ii++) {
2245        if (PieceType[color][ii] != -1) {
2246          col = PieceCol[color][ii];
2247          row = PieceRow[color][ii];
2248          Board[col][row] = (1-2*color)*(PieceType[color][ii]);
2249        }
2250      }
2251    }
2252
2253    ll++;
2254    if (ll >= FenString.length) {
2255      myAlertFEN(FenString, "missing castling availability");
2256      FenString += " " + assumedCastleRights() + " - 0 1";
2257      ll++;
2258    }
2259    CastlingLong = [-1, -1];
2260    CastlingShort = [-1, -1];
2261    cc = FenString.charAt(ll++);
2262    while (cc!=" ") {
2263      if (cc === PiecesArr[0].toUpperCase()) {
2264        for (CastlingShort[0] = 7; CastlingShort[0] > PieceCol[0][0]; CastlingShort[0]--) {
2265          if (Board[CastlingShort[0]][0] == 3) { break; }
2266        }
2267        if (CastlingShort[0] <= PieceCol[0][0]) {
2268          myAlertFEN(FenString, "missing castling Rook " + cc);
2269          CastlingShort[0] = -1;
2270        }
2271      } else if (cc === PiecesArr[1].toUpperCase()) {
2272        for (CastlingLong[0] = 0; CastlingLong[0] < PieceCol[0][0]; CastlingLong[0]++) {
2273          if (Board[CastlingLong[0]][0] == 3) { break; }
2274        }
2275        if (CastlingLong[0] >= PieceCol[0][0]) {
2276          myAlertFEN(FenString, "missing castling Rook " + cc);
2277          CastlingLong[0] = -1;
2278        }
2279      } else if (cc === PiecesArr[0].toLowerCase()) {
2280        for (CastlingShort[1] = 7; CastlingShort[1] > PieceCol[1][0]; CastlingShort[1]--) {
2281          if (Board[CastlingShort[1]][7] == -3) { break; }
2282        }
2283        if (CastlingShort[1] <= PieceCol[1][0]) {
2284          myAlertFEN(FenString, "missing castling Rook " + cc);
2285          CastlingShort[1] = -1;
2286        }
2287      } else if (cc === PiecesArr[1].toLowerCase()) {
2288        for (CastlingLong[1] = 0; CastlingLong[1] < PieceCol[1][0]; CastlingLong[1]++) {
2289          if (Board[CastlingLong[1]][7] == -3) { break; }
2290        }
2291        if (CastlingLong[1] >= PieceCol[1][0]) {
2292          myAlertFEN(FenString, "missing castling Rook " + cc);
2293          CastlingLong[1] = -1;
2294        }
2295      }
2296      castlingRookCol = columnsLetters.toUpperCase().indexOf(cc);
2297      if (castlingRookCol >= 0) { color = 0; }
2298      else {
2299        castlingRookCol = columnsLetters.toLowerCase().indexOf(cc);
2300        if (castlingRookCol >= 0) { color = 1; }
2301      }
2302      if (castlingRookCol >= 0) {
2303        if (Board[castlingRookCol][color*7] == (1-2*color) * 3) {
2304          if (castlingRookCol > PieceCol[color][0]) { CastlingShort[color] = castlingRookCol; }
2305          if (castlingRookCol < PieceCol[color][0]) { CastlingLong[color] = castlingRookCol; }
2306        } else {
2307          myAlertFEN(FenString, "missing castling Rook " + cc);
2308        }
2309      }
2310      cc = ll<FenString.length ? FenString.charAt(ll++) : " ";
2311    }
2312
2313    if (ll >= FenString.length) {
2314      myAlertFEN(FenString, "missing en passant square");
2315      FenString += " - 0 1";
2316      ll++;
2317    }
2318    cc = FenString.charAt(ll++);
2319    while (cc != " ") {
2320      if ((cc.charCodeAt(0)-97 >= 0) && (cc.charCodeAt(0)-97 <= 7)) {
2321        newEnPassant = true;
2322        newEnPassantCol = cc.charCodeAt(0)-97;
2323      }
2324      cc = ll<FenString.length ? FenString.charAt(ll++) : " ";
2325    }
2326    if (ll >= FenString.length) {
2327      myAlertFEN(FenString, "missing halfmove clock");
2328      FenString += " 0 1";
2329      ll++;
2330    }
2331    InitialHalfMoveClock = 0;
2332    cc = FenString.charAt(ll++);
2333    while (cc != " ") {
2334      if (isNaN(cc)) {
2335        myAlertFEN(FenString, "invalid halfmove clock");
2336        break;
2337      }
2338      InitialHalfMoveClock=InitialHalfMoveClock*10+parseInt(cc,10);
2339      cc = ll<FenString.length ? FenString.charAt(ll++) : " ";
2340    }
2341    if (ll >= FenString.length) {
2342      myAlertFEN(FenString, "missing fullmove number");
2343      FenString += " 1";
2344      ll++;
2345    }
2346
2347    fullMoveNumber = 0;
2348    cc = FenString.charAt(ll++);
2349    while (cc != " ") {
2350      if (isNaN(cc)) {
2351        myAlertFEN(FenString, "invalid fullmove number");
2352        fullMoveNumber = 1;
2353        break;
2354      }
2355      fullMoveNumber = fullMoveNumber*10+parseInt(cc,10);
2356      cc = ll<FenString.length ? FenString.charAt(ll++) : " ";
2357    }
2358    if (fullMoveNumber === 0) {
2359      myAlertFEN(FenString, "invalid fullmove 0 set to 1");
2360      fullMoveNumber = 1;
2361    }
2362    StartPly += 2*(fullMoveNumber-1);
2363
2364    HistEnPassant[StartPly] = newEnPassant;
2365    HistEnPassantCol[StartPly] = newEnPassantCol;
2366    HistNull[StartPly] = 0;
2367    HistVar[StartPly] = 0;
2368  }
2369}
2370
2371// castling rights assuming Kings and Rooks starting positions as in normal chess
2372function assumedCastleRights() {
2373  var ii, rights = "";
2374  if ((PieceRow[0][0] === 0) && (PieceCol[0][0] === 4)) {
2375    for (ii = 0; ii < PieceType[0].length; ii++) {
2376      if ((PieceType[0][ii] === 3) && (PieceRow[0][ii] === 0) && (PieceCol[0][ii] === 7)) {
2377        rights += PiecesArr[0].toUpperCase();
2378      } else if ((PieceType[0][ii] === 3) && (PieceRow[0][ii] === 0) && (PieceCol[0][ii] === 0)) {
2379        rights += PiecesArr[1].toUpperCase();
2380      }
2381    }
2382  }
2383  if ((PieceRow[1][0] === 7) && (PieceCol[1][0] === 4)) {
2384    for (ii = 0; ii < PieceType[1].length; ii++) {
2385      if ((PieceType[1][ii] === 3) && (PieceRow[1][ii] === 7) && (PieceCol[1][ii] === 7)) {
2386        rights += PiecesArr[0].toLowerCase();
2387      } else if ((PieceType[1][ii] === 3) && (PieceRow[1][ii] === 7) && (PieceCol[1][ii] === 0)) {
2388        rights += PiecesArr[1].toLowerCase();
2389      }
2390    }
2391  }
2392  return rights || "-";
2393}
2394
2395
2396function SetImageType(extension) {
2397  imageType = extension;
2398}
2399
2400function InitImages() {
2401  if (ImagePathOld === ImagePath) { return; }
2402
2403  if ((ImagePath.length > 0) && (ImagePath[ImagePath.length-1] != '/')) {
2404    ImagePath += '/';
2405  }
2406
2407  ClearImg = new Image();
2408  ClearImg.src = ImagePath + 'clear.' + imageType;
2409
2410  var ColorName = new Array ("w", "b");
2411  var PiecePrefix = new Array ("k", "q", "r", "b", "n", "p");
2412  for (var c=0; c<2; ++c) {
2413    for (var p=1; p<7; p++) {
2414      PieceImg[c][p] = new Image();
2415      PieceImg[c][p].src = ImagePath + ColorName[c] + PiecePrefix[p-1] + '.' + imageType;
2416    }
2417  }
2418  ImagePathOld = ImagePath;
2419}
2420
2421
2422function IsCheck(col, row, color) {
2423  var ii, jj;
2424  var sign = 2*color-1; // white or black
2425
2426  // other king giving check?
2427  if ((Math.abs(PieceCol[1-color][0]-col) <= 1) && (Math.abs(PieceRow[1-color][0]-row) <= 1)) { return true; }
2428
2429  // knight?
2430  for (ii = -2; ii <= 2; ii += 4) {
2431    for (jj = -1; jj <= 1; jj += 2) {
2432      if (SquareOnBoard(col+ii, row+jj)) {
2433        if (Board[col+ii][row+jj] == sign*5) { return true; }
2434      }
2435      if (SquareOnBoard(col+jj, row+ii)) {
2436        if (Board[col+jj][row+ii] == sign*5) { return true; }
2437      }
2438    }
2439  }
2440
2441  // pawn?
2442  for (ii = -1; ii <= 1; ii += 2) {
2443    if (SquareOnBoard(col+ii, row-sign)) {
2444      if (Board[col+ii][row-sign] == sign*6) { return true; }
2445    }
2446  }
2447
2448  // queens, rooks, bishops?
2449  for (ii = -1; ii <= 1; ++ii) {
2450    for (jj = -1; jj <= 1; ++jj) {
2451      if ((ii !== 0) || (jj !== 0)) {
2452        var checkCol = col+ii;
2453        var checkRow = row+jj;
2454        var thisPiece = 0;
2455
2456        while (SquareOnBoard(checkCol, checkRow) && (thisPiece === 0)) {
2457          thisPiece = Board[checkCol][checkRow];
2458          if (thisPiece === 0) {
2459            checkCol += ii;
2460            checkRow += jj;
2461          } else {
2462            if (thisPiece  == sign*2) { return true; }
2463            if ((thisPiece == sign*3) && ((ii === 0) || (jj === 0))) { return true; }
2464            if ((thisPiece == sign*4) && ((ii !== 0) && (jj !== 0))) { return true; }
2465          }
2466        }
2467      }
2468    }
2469  }
2470  return false;
2471}
2472
2473
2474function fixRegExp(exp) {
2475  return exp.replace(/([\[\]\(\)\{\}\.\*\+\^\$\|\?\\])/g, "\\$1");
2476}
2477
2478function LoadGameHeaders() {
2479  var ii;
2480  var parse;
2481
2482  gameEvent.length = gameSite.length = gameRound.length = gameDate.length = 0;
2483  gameWhite.length = gameBlack.length = gameResult.length = 0;
2484  gameSetUp.length = gameFEN.length = 0;
2485  gameInitialWhiteClock.length = gameInitialBlackClock.length = 0;
2486  gameVariant.length = 0;
2487
2488  pgnHeaderTagRegExpGlobal.lastIndex = 0; // resets global regular expression
2489  for (ii = 0; ii < numberOfGames; ++ii) {
2490    var ss = pgnHeader[ii];
2491    gameEvent[ii] = gameSite[ii] = gameRound[ii] = gameDate[ii] = "";
2492    gameWhite[ii] = gameBlack[ii] = gameResult[ii] = "";
2493    gameInitialWhiteClock[ii] = gameInitialBlackClock[ii] = "";
2494    gameVariant[ii] = "";
2495    while (parse = pgnHeaderTagRegExpGlobal.exec(ss)) {
2496      switch (parse[1]) {
2497        case 'Event': gameEvent[ii] = parse[2]; break;
2498        case 'Site': gameSite[ii] = parse[2]; break;
2499        case 'Round': gameRound[ii] = parse[2]; break;
2500        case 'Date': gameDate[ii] = parse[2]; break;
2501        case 'White': gameWhite[ii] = parse[2]; break;
2502        case 'Black': gameBlack[ii] = parse[2]; break;
2503        case 'Result': gameResult[ii] = parse[2]; break;
2504        case 'SetUp': gameSetUp[ii] = parse[2]; break;
2505        case 'FEN': gameFEN[ii] = parse[2]; break;
2506        case 'WhiteClock': gameInitialWhiteClock[ii] = parse[2]; break;
2507        case 'BlackClock': gameInitialBlackClock[ii] = parse[2]; break;
2508        case 'Variant': gameVariant[ii] = parse[2]; break;
2509        default: break;
2510      }
2511    }
2512  }
2513  if ((LiveBroadcastDemo) && (numberOfGames > 0)) {
2514    for (ii = 0; ii < numberOfGames; ++ii) {
2515      if ((gameDemoLength[ii] === undefined) || (gameDemoLength[ii] === 0)) {
2516        InitFEN(gameFEN[ii]);
2517        ParsePGNGameString(pgnGame[ii]);
2518        gameDemoLength[ii] = PlyNumber;
2519      }
2520      if (gameDemoMaxPly[ii] === undefined) { gameDemoMaxPly[ii] = 0; }
2521      if ((gameDemoMaxPly[ii] <= gameDemoLength[ii]) && (gameDemoLength[ii] > 0)) { gameResult[ii] = '*'; }
2522    }
2523  }
2524  return;
2525}
2526
2527
2528function MoveBackward(diff, scanOnly) {
2529
2530  // CurrentPly counts from 1, starting position 0
2531  var goFromPly  = CurrentPly - 1;
2532  var goToPly    = goFromPly  - diff;
2533  if (goToPly < StartPly) { goToPly = StartPly-1; }
2534
2535  // reconstruct old position
2536  for (var thisPly = goFromPly; thisPly > goToPly; --thisPly) {
2537    CurrentPly--;
2538    MoveColor = 1-MoveColor;
2539    CurrentVar = HistVar[thisPly];
2540    UndoMove(thisPly);
2541  }
2542
2543  if (scanOnly) { return; }
2544
2545  synchMoves();
2546
2547  // old position reconstructed: refresh board
2548  RefreshBoard();
2549  HighlightLastMove();
2550
2551  autoScrollToCurrentMoveIfEnabled();
2552
2553  // autoplay: restart timeout
2554  if (AutoPlayInterval) { clearTimeout(AutoPlayInterval); AutoPlayInterval = null; }
2555  if (isAutoPlayOn) {
2556    if (goToPly >= StartPlyVar[CurrentVar]) { AutoPlayInterval=setTimeout("MoveBackward(1)", Delay); }
2557    else { SetAutoPlay(false); }
2558  }
2559
2560  customFunctionOnMove();
2561  if (typeof(engineWinOnMove) == "function") { engineWinOnMove(); }
2562}
2563
2564function MoveForward(diff, targetVar, scanOnly) {
2565  var nextVar, nextVarStartPly, move, text;
2566  var oldVar = -1;
2567
2568  if (typeof(targetVar) == "undefined") { targetVar = CurrentVar; }
2569
2570  // CurrentPly counts from 1, starting position 0
2571  var goToPly = CurrentPly + parseInt(diff,10);
2572
2573  if (goToPly > StartPlyVar[targetVar] + PlyNumberVar[targetVar]) {
2574    goToPly = StartPlyVar[targetVar] + PlyNumberVar[targetVar];
2575  }
2576
2577  // reach to selected move checking legality
2578  for (var thisPly = CurrentPly; thisPly < goToPly; ++thisPly) {
2579
2580    if (targetVar !== CurrentVar) {
2581      for (var ii = 0; ii < PredecessorsVars[targetVar].length; ii++) {
2582        if (PredecessorsVars[targetVar][ii] === CurrentVar) { break; }
2583      }
2584      if (ii === PredecessorsVars[targetVar].length) {
2585        myAlert("error: unknown path to variation " + targetVar + " from " + CurrentVar + " in game " + (currentGame+1), true);
2586        return;
2587      } else {
2588        nextVarStartPly = StartPlyVar[PredecessorsVars[targetVar][ii + 1]];
2589        for (ii = ii+1; ii < PredecessorsVars[targetVar].length - 1; ii++) {
2590          if (StartPlyVar[PredecessorsVars[targetVar][ii+1]] !== StartPlyVar[PredecessorsVars[targetVar][ii]] ) { break; }
2591        }
2592        nextVar = PredecessorsVars[targetVar][ii];
2593      }
2594    } else { nextVar = nextVarStartPly = -1; }
2595
2596    if (thisPly === nextVarStartPly) {
2597      oldVar = CurrentVar;
2598      CurrentVar = nextVar;
2599    }
2600
2601    if (typeof(move = MovesVar[CurrentVar][thisPly]) == "undefined") { break; }
2602
2603    if (ParseLastMoveError = !ParseMove(move, thisPly)) {
2604      text = (Math.floor(thisPly / 2) + 1) + ((thisPly % 2) === 0 ? '. ' : '... ');
2605      myAlert('error: invalid ply ' + text + move + ' in game ' + (currentGame+1) + ' variation ' + CurrentVar, true);
2606      if (thisPly === nextVarStartPly) { CurrentVar = oldVar; }
2607      break;
2608    }
2609    MoveColor = 1-MoveColor;
2610
2611  }
2612
2613  // new position: update ply count, then refresh board
2614  CurrentPly = thisPly;
2615
2616  if (scanOnly) { return; }
2617
2618  synchMoves();
2619
2620  RefreshBoard();
2621  HighlightLastMove();
2622
2623  autoScrollToCurrentMoveIfEnabled();
2624
2625  // autoplay: restart timeout
2626  if (AutoPlayInterval) { clearTimeout(AutoPlayInterval); AutoPlayInterval = null; }
2627  if (ParseLastMoveError) { SetAutoPlay(false); }
2628  else if (thisPly == goToPly) {
2629    if (isAutoPlayOn) {
2630      if (goToPly < StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]) {
2631        AutoPlayInterval=setTimeout("MoveForward(1)", Delay);
2632      } else {
2633        if (autoplayNextGame && (CurrentVar === 0)) { AutoPlayInterval=setTimeout("AutoplayNextGame()", Delay); }
2634        else { SetAutoPlay(false); }
2635      }
2636    }
2637  }
2638
2639  customFunctionOnMove();
2640  if (typeof(engineWinOnMove) == "function") { engineWinOnMove(); }
2641}
2642
2643var lastSynchCurrentVar = -1;
2644function synchMoves() {
2645  var start, end;
2646  if (CurrentVar === lastSynchCurrentVar) { return; }
2647  Moves = new Array();
2648  MoveComments = new Array();
2649  for (var ii = 0; ii < PredecessorsVars[CurrentVar].length; ii++) {
2650    start = StartPlyVar[PredecessorsVars[CurrentVar][ii]];
2651    if (ii < PredecessorsVars[CurrentVar].length - 1) {
2652      end = StartPlyVar[PredecessorsVars[CurrentVar][ii+1]];
2653    } else {
2654      end = StartPlyVar[PredecessorsVars[CurrentVar][ii]] + PlyNumberVar[PredecessorsVars[CurrentVar][ii]];
2655    }
2656    for (var jj = start; jj < end; jj++) {
2657      Moves[jj] = MovesVar[PredecessorsVars[CurrentVar][ii]][jj];
2658      MoveComments[jj] = MoveCommentsVar[PredecessorsVars[CurrentVar][ii]][jj] || "";
2659    }
2660  }
2661  MoveComments[jj] = MoveCommentsVar[PredecessorsVars[CurrentVar][ii-1]][jj] || "";
2662  PlyNumber = StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar] - StartPly;
2663  lastSynchCurrentVar = CurrentVar;
2664}
2665
2666
2667function AutoplayNextGame() {
2668  if (fatalErrorNumSinceReset === 0) {
2669    if (numberOfGames > 0) {
2670      Init((currentGame + 1) % numberOfGames);
2671      if ((numberOfGames > 1) || (PlyNumber > 0)) {
2672        SetAutoPlay(true);
2673        return;
2674      }
2675    }
2676  }
2677  SetAutoPlay(false);
2678}
2679
2680function MoveToNextComment(varOnly) {
2681  for (var ii=CurrentPly+1; ii<=StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]; ii++) {
2682    if (MoveComments[ii].match(pgn4webVariationRegExp) || (!varOnly && strippedMoveComment(ii))) { GoToMove(ii); break; }
2683  }
2684}
2685
2686function MoveToPrevComment(varOnly) {
2687  for (var ii=(CurrentPly-1); ii>=StartPly; ii--) {
2688    if ((ii > 0 || CurrentVar > 0) && ii === StartPlyVar[HistVar[ii+1]]) { GoToMove(ii+1, HistVar[ii]); break; }
2689    if (MoveComments[ii].match(pgn4webVariationRegExp) || (!varOnly && strippedMoveComment(ii))) { GoToMove(ii); break; }
2690  }
2691}
2692
2693
2694function OpenGame(gameId) {
2695  ParsePGNGameString(pgnGame[gameId]);
2696  currentGame = gameId;
2697  ParseLastMoveError = false;
2698
2699  if (LiveBroadcastDemo) {
2700    if (gameDemoMaxPly[gameId] <= PlyNumber) { PlyNumber = PlyNumberVar[0] = gameDemoMaxPly[gameId]; }
2701  }
2702
2703  PrintHTML();
2704}
2705
2706var CurrentVar = -1;
2707var lastVarWithNoMoves;
2708var numberOfVars;
2709var MovesVar;
2710var MoveCommentsVar;
2711var GameHasComments;
2712var GameHasVariations;
2713var StartPlyVar;
2714var PlyNumberVar;
2715var CurrentVarStack;
2716var PlyNumberStack;
2717var PredecessorsVars;
2718
2719function initVar () {
2720  MovesVar = new Array();
2721  MoveCommentsVar = new Array();
2722  GameHasComments = false;
2723  GameHasVariations = false;
2724  StartPlyVar = new Array();
2725  PlyNumberVar = new Array();
2726  CurrentVar = -1;
2727  lastVarWithNoMoves = [false];
2728  numberOfVars = 0;
2729  CurrentVarStack = new Array();
2730  PlyNumber = 1;
2731  PlyNumberStack = new Array();
2732  PredecessorsVars = new Array();
2733  startVar(false);
2734}
2735
2736function startVar(isContinuation) {
2737  if (CurrentVar >= 0) {
2738    CurrentVarStack.push(CurrentVar);
2739    PlyNumberStack.push(PlyNumber);
2740  }
2741  CurrentVar = numberOfVars++;
2742  PredecessorsVars[CurrentVar] = CurrentVarStack.slice(0);
2743  PredecessorsVars[CurrentVar].push(CurrentVar);
2744  MovesVar[CurrentVar] = new Array();
2745  MoveCommentsVar[CurrentVar] = new Array();
2746  if (!isContinuation) {
2747    if (lastVarWithNoMoves[lastVarWithNoMoves.length - 1]) {
2748      myAlert("warning: malformed PGN data in game " + (currentGame+1) + ": variation " + CurrentVar + " starting before parent", true);
2749    } else {
2750      PlyNumber -= 1;
2751    }
2752  }
2753  lastVarWithNoMoves.push(true);
2754  MoveCommentsVar[CurrentVar][StartPly + PlyNumber] = "";
2755  StartPlyVar[CurrentVar] = StartPly + PlyNumber;
2756}
2757
2758function closeVar() {
2759  if (StartPly + PlyNumber === StartPlyVar[CurrentVar]) {
2760    myAlert("warning: empty variation " + CurrentVar + " in game " + (currentGame+1), false);
2761  } else {
2762    GameHasVariations = true;
2763  }
2764  lastVarWithNoMoves.pop();
2765  PlyNumberVar[CurrentVar] = StartPly + PlyNumber - StartPlyVar[CurrentVar];
2766  for (var ii=StartPlyVar[CurrentVar]; ii<=StartPlyVar[CurrentVar]+PlyNumberVar[CurrentVar]; ii++) {
2767    if (MoveCommentsVar[CurrentVar][ii]) {
2768      MoveCommentsVar[CurrentVar][ii] = MoveCommentsVar[CurrentVar][ii].replace(/\s+/g, ' ');
2769      MoveCommentsVar[CurrentVar][ii] = translateNAGs(MoveCommentsVar[CurrentVar][ii]);
2770      MoveCommentsVar[CurrentVar][ii] = MoveCommentsVar[CurrentVar][ii].replace(/\s+$/g, '');
2771    } else {
2772      MoveCommentsVar[CurrentVar][ii] = '';
2773    }
2774  }
2775  if (CurrentVarStack.length) {
2776    CurrentVar = CurrentVarStack.pop();
2777    PlyNumber = PlyNumberStack.pop();
2778  } else {
2779    myAlert("error: closeVar error" + " in game " + (currentGame+1), true);
2780  }
2781}
2782
2783function childrenVars(thisPly, thisVar) {
2784  if (typeof(thisVar) == "undefined") { thisVar = CurrentVar; }
2785  if (typeof(thisPly) == "undefined") { thisPly = CurrentPly; }
2786  var children = new Array();
2787  for (var ii = thisVar; ii < numberOfVars; ii++) {
2788    if ((ii === thisVar && StartPlyVar[ii] + PlyNumberVar[ii] > thisPly) || (realParentVar(ii) === thisVar && StartPlyVar[ii] === thisPly && PlyNumberVar[ii] > 0)) {
2789      children.push(ii);
2790    }
2791  }
2792  return children;
2793}
2794
2795function realParentVar(childVar) {
2796  for (var ii = PredecessorsVars[childVar].length - 1; ii > 0; ii--) {
2797    if (StartPlyVar[PredecessorsVars[childVar][ii]] !== StartPlyVar[PredecessorsVars[childVar][ii-1]]) {
2798      return PredecessorsVars[childVar][ii-1];
2799    }
2800  }
2801  return PredecessorsVars[childVar][ii];
2802}
2803
2804function goToNextVariationSibling() {
2805  if (CurrentPly === StartPly) { return false; }
2806  var siblings = childrenVars(CurrentPly - 1, HistVar[CurrentPly - 1]);
2807  if (siblings.length < 2) { return false; }
2808  for (var ii = 0; ii < siblings.length; ii++) {
2809    if (siblings[ii] === CurrentVar) { break; }
2810  }
2811  if (siblings[ii] !== CurrentVar) { return false; }
2812  GoToMove(CurrentPly, siblings[(ii + 1) % siblings.length]);
2813  return true;
2814}
2815
2816function goToFirstChild() {
2817  var children = childrenVars(CurrentPly, CurrentVar);
2818  if (children.length < 1) { return false; }
2819  if (children[0] === CurrentVar) {
2820    if (children.length < 2) { return false; }
2821    GoToMove(CurrentPly + 1, children[1]);
2822  } else {
2823    GoToMove(CurrentPly + 1, children[0]);
2824  }
2825  return true;
2826}
2827
2828function ParsePGNGameString(gameString) {
2829  var ii, start, end, move, moveCount, needle, commentStart, commentEnd, isContinuation;
2830
2831  var ssRep, ss = gameString, ssComm;
2832  ss = ss.replace(pgn4webVariationRegExpGlobal, "[%_pgn4web_variation_ $1]");
2833  // empty variations to comments
2834  while ((ssRep = ss.replace(/\((([\?!+#\s]|\$\d+|{[^}]*})*)\)/g, ' $1 ')) !== ss) { ss = ssRep; }
2835  ss = ss.replace(/^\s/, '');
2836  ss = ss.replace(/\s$/, '');
2837
2838  initVar ();
2839
2840  PlyNumber = 0;
2841
2842  for (start=0; start<ss.length; start++) {
2843
2844    switch (ss.charAt(start)) {
2845
2846      case ' ':
2847      case '\b':
2848      case '\f':
2849      case '\n':
2850      case '\r':
2851      case '\t':
2852        break;
2853
2854      case '$':
2855        commentStart = start;
2856        commentEnd = commentStart + 1;
2857        while ('0123456789'.indexOf(ss.charAt(commentEnd)) >= 0) {
2858          commentEnd++;
2859          if (commentEnd >= ss.length) { break; }
2860        }
2861        if (MoveCommentsVar[CurrentVar][StartPly+PlyNumber]) { MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ' '; }
2862        MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += translateNAGs(ss.substring(commentStart, commentEnd).replace(/(^\s*|\s*$)/, ''));
2863        start = commentEnd - 1;
2864        break;
2865
2866      case '!':
2867      case '?':
2868        commentStart = start;
2869        commentEnd = commentStart + 1;
2870        while ('!?'.indexOf(ss.charAt(commentEnd)) >= 0) {
2871          commentEnd++;
2872          if (commentEnd >= ss.length) { break; }
2873        }
2874        if (MoveCommentsVar[CurrentVar][StartPly+PlyNumber]) { MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ' '; }
2875        MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ss.substring(commentStart, commentEnd);
2876        start = commentEnd - 1;
2877        break;
2878
2879      case '{':
2880        commentStart = start+1;
2881        commentEnd = ss.indexOf('}',start+1);
2882        if (commentEnd < 0) {
2883          myAlert('error: missing end comment } in game ' + (currentGame+1), true);
2884          commentEnd = ss.length;
2885        }
2886        if (MoveCommentsVar[CurrentVar][StartPly+PlyNumber]) { MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ' '; }
2887        ssComm = translateNAGs(ss.substring(commentStart, commentEnd).replace(/(^\s*|\s*$)/, ''));
2888        MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ssComm;
2889        GameHasComments = GameHasComments || ssComm.replace(/\[%[^\]]*\]\s*/g,'').replace(basicNAGs, '').replace(/^\s+$/,'') !== '';
2890        start = commentEnd;
2891        break;
2892
2893      case '%':
2894        // % must be first char of the line
2895        if ((start > 0) && (ss.charAt(start-1) != '\n')) { break; }
2896        commentStart = start+1;
2897        commentEnd = ss.indexOf('\n',start+1);
2898        if (commentEnd < 0) { commentEnd = ss.length; }
2899        start = commentEnd;
2900        break;
2901
2902      case ';':
2903        commentStart = start+1;
2904        commentEnd = ss.indexOf('\n',start+1);
2905        if (commentEnd < 0) { commentEnd = ss.length; }
2906        if (MoveCommentsVar[CurrentVar][StartPly+PlyNumber]) { MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ' '; }
2907        ssComm = translateNAGs(ss.substring(commentStart, commentEnd).replace(/(^\s*|\s*$)/, ''));
2908        MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ssComm;
2909        GameHasComments = GameHasComments || ssComm.replace(/\[%[^\]]*\]\s*/g,'').replace(basicNAGs, '').replace(/^\s+$/,'') !== '';
2910        start = commentEnd;
2911        break;
2912
2913      case '(':
2914        if (isContinuation = (ss.charAt(start+1) == '*')) { start += 1; }
2915        MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ' [%pgn4web_variation ' + numberOfVars + '] ';
2916        startVar(isContinuation);
2917        break;
2918
2919      case ')':
2920        closeVar();
2921        break;
2922
2923      default:
2924
2925        needle = new Array('1-0', '0-1', '1/2-1/2', '*');
2926        for (ii=0; ii<needle.length; ii++) {
2927          if (ss.indexOf(needle[ii],start)==start) {
2928            if (CurrentVar === 0) { end = ss.length; }
2929            else {
2930              end = start + needle[ii].length;
2931              if (MoveCommentsVar[CurrentVar][StartPly+PlyNumber]) { MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += ' '; }
2932              MoveCommentsVar[CurrentVar][StartPly+PlyNumber] += needle[ii];
2933            }
2934            start = end;
2935            break;
2936          }
2937        }
2938        if (start == ss.length) { break; }
2939
2940        moveCount = Math.floor((StartPly+PlyNumber)/2)+1;
2941        needle = moveCount.toString();
2942        if (ss.indexOf(needle,start)==start) {
2943          start += needle.length;
2944          while (' .\n\r'.indexOf(ss.charAt(start)) != -1) { start++; }
2945        }
2946
2947        if ((end = start + ss.substr(start).search(/[\s${;!?()]/)) < start) { end = ss.length; }
2948        move = ss.substring(start,end);
2949        MovesVar[CurrentVar][StartPly+PlyNumber] = CleanMove(move);
2950        lastVarWithNoMoves[lastVarWithNoMoves.length - 1] = false;
2951        if (ss.charAt(end) == ' ') { start = end; }
2952        else { start = end - 1; }
2953        if (!MovesVar[CurrentVar][StartPly+PlyNumber].match(/^[\s+#]*$/)) { // to cope with malsformed PGN data
2954          PlyNumber++;
2955          MoveCommentsVar[CurrentVar][StartPly+PlyNumber] = '';
2956        }
2957        break;
2958    }
2959  }
2960
2961  if (CurrentVar !== 0) {
2962    myAlert("error: ParsePGNGameString ends with CurrentVar " + CurrentVar + " in game " + (currentGame+1), true);
2963    while (CurrentVar > 0) { closeVar(); }
2964  }
2965
2966  StartPlyVar[0] = StartPly;
2967  PlyNumberVar[0] = PlyNumber;
2968
2969  GameHasComments = GameHasComments || GameHasVariations;
2970
2971  lastSynchCurrentVar = -1;
2972}
2973
2974var NAGstyle = 'default';
2975var NAG = new Array();
2976NAG[0] = '';
2977NAG[1] = '!'; // 'good move';
2978NAG[2] = '?'; // 'bad move';
2979NAG[3] = '!!'; // 'very good move';
2980NAG[4] = '??'; // 'very bad move';
2981NAG[5] = '!?'; // 'speculative move';
2982NAG[6] = '?!'; // 'questionable move';
2983NAG[7] = 'forced move'; // '[]';
2984NAG[8] = 'singular move'; // '[]';
2985NAG[9] = 'worst move'; // '??';
2986NAG[10] = 'drawish position'; // '=';
2987NAG[11] = 'equal chances, quiet position'; // '=';
2988NAG[12] = 'equal chances, active position'; // '=';
2989NAG[13] = 'unclear position'; // '~~';
2990NAG[14] = 'White has a slight advantage'; // NAG[15] = '+/=';
2991NAG[16] = 'White has a moderate advantage'; // NAG[17] = '+/-';
2992NAG[18] = 'White has a decisive advantage'; // NAG[19] = '+-';
2993NAG[20] = 'White has a crushing advantage'; // NAG[21] = '+-';
2994NAG[22] = 'White is in zugzwang'; // NAG[23] = '(.)';
2995NAG[24] = 'White has a slight space advantage'; // NAG[25] = '()';
2996NAG[26] = 'White has a moderate space advantage'; // NAG[27] = '()';
2997NAG[28] = 'White has a decisive space advantage'; // NAG[29] = '()';
2998NAG[30] = 'White has a slight time (development) advantage'; // NAG[31] = '@';
2999NAG[32] = 'White has a moderate time (development) advantage'; // NAG[33] = '@';
3000NAG[34] = 'White has a decisive time (development) advantage'; // NAG[35] = '@';
3001NAG[36] = 'White has the initiative'; // NAG[37] = '|^';
3002NAG[38] = 'White has a lasting initiative'; // NAG[39] = '|^';
3003NAG[40] = 'White has the attack'; // NAG[41] = '->';
3004NAG[42] = 'White has insufficient compensation for material deficit';
3005NAG[44] = 'White has sufficient compensation for material deficit'; // NAG[45] = '=/~';
3006NAG[46] = 'White has more than adequate compensation for material deficit'; // NAG[47] = '=/~';
3007NAG[48] = 'White has a slight center control advantage'; // NAG[49] = '[+]';
3008NAG[50] = 'White has a moderate center control advantage'; // NAG[51] = '[+]';
3009NAG[52] = 'White has a decisive center control advantage'; // NAG[53] = '[+]';
3010NAG[54] = 'White has a slight kingside control advantage'; // NAG[55] = '>>';
3011NAG[56] = 'White has a moderate kingside control advantage'; // NAG[57] = '>>';
3012NAG[58] = 'White has a decisive kingside control advantage'; // NAG[59] = '>>';
3013NAG[60] = 'White has a slight queenside control advantage'; // NAG[61] = '<<';
3014NAG[62] = 'White has a moderate queenside control advantage'; // NAG[63] = '<<';
3015NAG[64] = 'White has a decisive queenside control advantage'; // NAG[65] = '<<';
3016NAG[66] = 'White has a vulnerable first rank';
3017NAG[68] = 'White has a well protected first rank';
3018NAG[70] = 'White has a poorly protected king';
3019NAG[72] = 'White has a well protected king';
3020NAG[74] = 'White has a poorly placed king';
3021NAG[76] = 'White has a well placed king';
3022NAG[78] = 'White has a very weak pawn structure';
3023NAG[80] = 'White has a moderately weak pawn structure';
3024NAG[82] = 'White has a moderately strong pawn structure';
3025NAG[84] = 'White has a very strong pawn structure';
3026NAG[86] = 'White has poor knight placement';
3027NAG[88] = 'White has good knight placement';
3028NAG[90] = 'White has poor bishop placement';
3029NAG[92] = 'White has good bishop placement';
3030NAG[94] = 'White has poor rook placement';
3031NAG[96] = 'White has good rook placement';
3032NAG[98] = 'White has poor queen placement';
3033NAG[100] = 'White has good queen placement';
3034NAG[102] = 'White has poor piece coordination';
3035NAG[104] = 'White has good piece coordination';
3036NAG[106] = 'White has played the opening very poorly';
3037NAG[108] = 'White has played the opening poorly';
3038NAG[110] = 'White has played the opening well';
3039NAG[112] = 'White has played the opening very well';
3040NAG[114] = 'White has played the middlegame very poorly';
3041NAG[116] = 'White has played the middlegame poorly';
3042NAG[118] = 'White has played the middlegame well';
3043NAG[120] = 'White has played the middlegame very well';
3044NAG[122] = 'White has played the ending very poorly';
3045NAG[124] = 'White has played the ending poorly';
3046NAG[126] = 'White has played the ending well';
3047NAG[128] = 'White has played the ending very well';
3048NAG[130] = 'White has slight counterplay'; // NAG[131] = '<=>';
3049NAG[132] = 'White has moderate counterplay'; // NAG[133] = '<=>';
3050NAG[134] = 'White has decisive counterplay'; // NAG[135] = '<=>';
3051NAG[136] = 'White has moderate time control pressure'; // NAG[137] = '(+)';
3052NAG[138] = 'White has severe time control pressure'; // NAG[139] = '(+)';
3053
3054for (i=14; i<139; i+=2) { NAG[i+1] = NAG[i].replace("White", "Black"); }
3055
3056function translateNAGs(comment) {
3057  var matches = comment.match(/\$+[0-9]+/g);
3058  if (matches) {
3059    for (var ii = 0; ii < matches.length; ii++) {
3060      var nag = matches[ii].substr(1);
3061      if (NAG[nag] !== undefined) {
3062        comment = comment.replace(new RegExp("\\$+" + nag + "(?!\\d)"), NAG[nag]);
3063      }
3064    }
3065  }
3066  return comment;
3067}
3068
3069function ParseMove(move, plyCount) {
3070  var ii, ll;
3071  var rem;
3072  var toRowMarker = -1;
3073
3074  castleRook = -1;
3075  mvIsCastling = 0;
3076  mvIsPromotion = 0;
3077  mvCapture = 0;
3078  mvFromCol = -1;
3079  mvFromRow = -1;
3080  mvToCol = -1;
3081  mvToRow = -1;
3082  mvPiece = -1;
3083  mvPieceId = -1;
3084  mvPieceOnTo = -1;
3085  mvCaptured = -1;
3086  mvCapturedId = -1;
3087  mvIsNull = 0;
3088
3089  if (typeof(move) == "undefined") { return false; }
3090
3091  HistEnPassant[plyCount+1] = false;
3092  HistEnPassantCol[plyCount+1] = -1;
3093
3094  if (move.indexOf('--') === 0) {
3095    mvIsNull = 1;
3096    CheckLegality('--', plyCount);
3097    return true;
3098  }
3099
3100  // get destination column/row remembering what's left e.g. Rdxc3 exf8=Q#
3101  for (ii = move.length-1; ii > 0; ii--) {
3102    if (!isNaN(move.charAt(ii))) {
3103      mvToCol = move.charCodeAt(ii-1) - 97;
3104      mvToRow = move.charAt(ii) - 1;
3105      rem = move.substring(0, ii-1);
3106      toRowMarker = ii;
3107      break;
3108    }
3109  }
3110
3111  // final square did not make sense: maybe a castle?
3112  if ((mvToCol < 0) || (mvToCol > 7) || (mvToRow < 0) || (mvToRow > 7)) {
3113    // long castling first: looking for o-o will get o-o-o too
3114    if (move.indexOf('O-O-O') === 0) {
3115      mvIsCastling = 1;
3116      mvPiece = 1;
3117      mvPieceId = 0;
3118      mvPieceOnTo = 1;
3119      mvFromCol = 4;
3120      mvToCol = 2;
3121      mvFromRow = 7*MoveColor;
3122      mvToRow = 7*MoveColor;
3123      return CheckLegality('O-O-O', plyCount);
3124    } else if (move.indexOf('O-O') === 0) {
3125      mvIsCastling = 1;
3126      mvPiece = 1;
3127      mvPieceId = 0;
3128      mvPieceOnTo = 1;
3129      mvFromCol = 4;
3130      mvToCol = 6;
3131      mvFromRow = 7*MoveColor;
3132      mvToRow = 7*MoveColor;
3133      return CheckLegality('O-O', plyCount);
3134    } else { return false; }
3135  }
3136
3137  rem = rem.replace(/-/g, '');
3138  // get piece and origin square: mark captures ('x' is there)
3139  ll = rem.length;
3140  if (ll > 4) { return false; }
3141  mvPiece = -1; // make sure mvPiece is properly assigned later
3142  if (ll === 0) { mvPiece = 6; }
3143  else {
3144    for (ii = 5; ii > 0; ii--) { if (rem.charAt(0) == PiecesArr[ii-1]) { mvPiece = ii; break; } }
3145    if (mvPiece == -1) { if (columnsLetters.toLowerCase().indexOf(rem.charAt(0)) >= 0) { mvPiece = 6; } }
3146    if (mvPiece == -1) { return false; }
3147    if (rem.charAt(ll-1) == 'x') { mvCapture = 1; }
3148    if (isNaN(move.charAt(ll-1-mvCapture))) {
3149      mvFromCol = move.charCodeAt(ll-1-mvCapture) - 97;
3150      if ((mvFromCol < 0) || (mvFromCol > 7)) { mvFromCol = -1; }
3151    } else {
3152      mvFromRow = move.charAt(ll-1-mvCapture) - 1;
3153      if ((mvFromRow < 0) || (mvFromRow > 7)) { mvFromRow = -1; }
3154      else {
3155        mvFromCol = move.charCodeAt(ll-2-mvCapture) - 97;
3156        if ((mvFromCol < 0) || (mvFromCol > 7)) { mvFromCol = -1; }
3157      }
3158    }
3159
3160    if ( (ll > 1) && (!mvCapture) && (mvFromCol == -1) && (mvFromRow == -1) ) { return false; }
3161    if ( (mvPiece == 6) && (!mvCapture) && (mvFromCol == -1) && (mvFromRow == -1) ) { return false; }
3162  }
3163
3164  // "square to" occupied: capture (note en-passant case)
3165  if ((Board[mvToCol][mvToRow] !== 0) || ((mvPiece == 6) && (HistEnPassant[plyCount]) && (mvToCol == HistEnPassantCol[plyCount]) && (mvToRow == 5-3*MoveColor))) { mvCapture = 1; }
3166
3167  mvPieceOnTo = mvPiece;
3168  if (mvPiece == 6) {
3169    // move contains '=' or char after destination row: might be a promotion
3170    ii = move.indexOf('=');
3171    if (ii < 0) { ii = toRowMarker; }
3172    if ((ii > 0) && (ii < move.length-1)) {
3173      var newPiece = move.charAt(ii+1);
3174      if (newPiece == PiecesArr[1]) { mvPieceOnTo = 2; }
3175      else if (newPiece == PiecesArr[2]) { mvPieceOnTo = 3; }
3176      else if (newPiece == PiecesArr[3]) { mvPieceOnTo = 4; }
3177      else if (newPiece == PiecesArr[4]) { mvPieceOnTo = 5; }
3178      if (mvPieceOnTo != mvPiece) { mvIsPromotion = 1; }
3179    }
3180    if ((mvToRow == 7 * (1-MoveColor)) ? !mvIsPromotion : mvIsPromotion) { return false; }
3181  }
3182
3183  // which captured piece: if nothing found must be en-passant
3184  if (mvCapture) {
3185    for (mvCapturedId = 15; mvCapturedId >= 0; mvCapturedId--) {
3186      if ((PieceType[1-MoveColor][mvCapturedId] > 0) && (PieceCol[1-MoveColor][mvCapturedId] == mvToCol) && (PieceRow[1-MoveColor][mvCapturedId] == mvToRow)) {
3187        mvCaptured = PieceType[1-MoveColor][mvCapturedId];
3188        if (mvCaptured == 1) { return false; }
3189        break;
3190      }
3191    }
3192    if ((mvPiece == 6) && (mvCapturedId < 1) && (HistEnPassant[plyCount])) {
3193      for (mvCapturedId = 15; mvCapturedId >= 0; mvCapturedId--) {
3194        if ((PieceType[1-MoveColor][mvCapturedId] == 6) && (PieceCol[1-MoveColor][mvCapturedId] == mvToCol) && (PieceRow[1-MoveColor][mvCapturedId] == 4-MoveColor)) {
3195          mvCaptured = PieceType[1-MoveColor][mvCapturedId];
3196          break;
3197        }
3198      }
3199    }
3200  }
3201
3202  // check move legality
3203  if (!CheckLegality(PiecesArr[mvPiece-1], plyCount)) { return false; }
3204
3205  // pawn moved: check en-passant possibility
3206  if (mvPiece == 6) {
3207     if (Math.abs(HistRow[0][plyCount]-mvToRow) == 2) {
3208       HistEnPassant[plyCount+1] = true;
3209       HistEnPassantCol[plyCount+1] = mvToCol;
3210     }
3211  }
3212  return true;
3213}
3214
3215function SetGameSelectorOptions(head, num, chEvent, chSite, chRound, chWhite, chBlack, chResult, chDate) {
3216  if (typeof(head) == "string") { gameSelectorHead = head; }
3217  gameSelectorNum = (num === true);
3218  gameSelectorChEvent  = Math.max(Math.min(chEvent , 32) || 0, 0) || 0;
3219  gameSelectorChSite   = Math.max(Math.min(chSite  , 32) || 0, 0) || 0;
3220  gameSelectorChRound  = Math.max(Math.min(chRound , 32) || 0, 0) || 0;
3221  gameSelectorChWhite  = Math.max(Math.min(chWhite , 32) || 0, 0) || 0;
3222  gameSelectorChBlack  = Math.max(Math.min(chBlack , 32) || 0, 0) || 0;
3223  gameSelectorChResult = Math.max(Math.min(chResult, 32) || 0, 0) || 0;
3224  gameSelectorChDate   = Math.max(Math.min(chDate  , 32) || 0, 0) || 0;
3225}
3226
3227var clickedSquareInterval = null;
3228function clickedSquare(ii, jj) {
3229  if (clickedSquareInterval) { return; } // dont trigger twice
3230  var squareId = 'tcol' + jj + 'trow' + ii;
3231  var theObj = document.getElementById(squareId);
3232  if (theObj) {
3233    var oldClass = theObj.className;
3234    theObj.className = (ii+jj)%2 === 0 ? "blackSquare" : "whiteSquare";
3235    clickedSquareInterval = setTimeout("reset_after_click(" + ii + "," + jj + ",'" + oldClass + "','" + theObj.className + "')", 66);
3236    clearSelectedText();
3237  }
3238}
3239
3240function reset_after_click (ii, jj, oldClass, newClass) {
3241  var theObj = document.getElementById('tcol' + jj + 'trow' + ii);
3242  if (theObj) {
3243    // square class changed again by pgn4web already: dont touch it anymore e.g. autoplay
3244    if (theObj.className == newClass) { theObj.className = oldClass; }
3245    clickedSquareInterval = null;
3246  }
3247}
3248
3249
3250var lastSearchPgnExpression = "";
3251function gameNumberSearchPgn(searchExpression, backward, includeCurrent) {
3252  lastSearchPgnExpression = searchExpression;
3253  if (searchExpression === "") { return false; }
3254  // replace newline with spaces so that we can use regexp "." on whole game
3255  var newlinesRegExp = new RegExp("[\n\r]", "gm");
3256  var searchExpressionRegExp = new RegExp(searchExpression, "im");
3257  // at start currentGame might still be -1
3258  var thisCurrentGame = (currentGame < 0) || (currentGame >= numberOfGames) ? 0 : currentGame;
3259  var needle = fullPgnGame(thisCurrentGame);
3260  if (includeCurrent && needle.replace(newlinesRegExp, " ").match(searchExpressionRegExp)) {
3261    return thisCurrentGame;
3262  }
3263  var delta = backward ? -1 : +1;
3264  for (var thisGame = (thisCurrentGame + delta + numberOfGames) % numberOfGames; thisGame != thisCurrentGame; thisGame = (thisGame + delta + numberOfGames) % numberOfGames) {
3265    needle = fullPgnGame(thisGame);
3266    if (needle.replace(newlinesRegExp, " ").match(searchExpressionRegExp)) {
3267      return thisGame;
3268    }
3269  }
3270  return false;
3271}
3272
3273function searchPgnGame(searchExpression, backward) {
3274  if (typeof(searchExpression) == "undefined") { searchExpression = ""; }
3275  lastSearchPgnExpression = searchExpression;
3276  var theObj = document.getElementById('searchPgnExpression');
3277  if (theObj) { theObj.value = searchExpression; }
3278  if ((searchExpression === "") || (numberOfGames < 2)) { return; }
3279  var thisGame = gameNumberSearchPgn(searchExpression, backward, false);
3280  if ((thisGame !== false) && (thisGame != currentGame)) { Init(thisGame); }
3281}
3282
3283function searchPgnGamePrompt() {
3284  if (numberOfGames < 2) {
3285    alert("info: search prompt disabled with less than 2 games");
3286    return;
3287  }
3288  var searchExpression = prompt("Please enter search pattern for PGN games:", lastSearchPgnExpression);
3289  if (searchExpression) { searchPgnGame(searchExpression); }
3290}
3291
3292function searchPgnGameForm() {
3293  var theObj = document.getElementById('searchPgnExpression');
3294  if (theObj) { searchPgnGame(document.getElementById('searchPgnExpression').value); }
3295}
3296
3297var 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");
3298function fixCommentForDisplay(comment) {
3299  return comment.replace(chessMovesRegExp, '<SPAN CLASS="commentMove">$1</SPAN>');
3300}
3301
3302var tableSize = 0;
3303var textSelectOptions = '';
3304function PrintHTML() {
3305  var ii, jj, text, theObj, squareId, imageId, squareCoord, squareTitle, numText, textSO;
3306
3307  // chessboard
3308
3309  if (theObj = document.getElementById("GameBoard")) {
3310    text = '<TABLE CLASS="boardTable" ID="boardTable" CELLSPACING=0 CELLPADDING=0';
3311    text += (tableSize > 0) ? ' STYLE="width: ' + tableSize + 'px; height: ' + tableSize + 'px;">' : '>';
3312    for (ii = 0; ii < 8; ++ii) {
3313      text += '<TR>';
3314      for (jj = 0; jj < 8; ++jj) {
3315        squareId = 'tcol' + jj + 'trow' + ii;
3316        imageId = 'img_' + squareId;
3317        text += (ii+jj)%2 === 0 ? '<TD CLASS="whiteSquare" ID="' + squareId + '" BGCOLOR="#FFFFFF"' : '<TD CLASS="blackSquare" ID="' + squareId + '" BGCOLOR="#D3D3D3"';
3318        text += ' ALIGN="center" VALIGN="middle" ONCLICK="clickedSquare(' + ii + ',' + jj + ')">';
3319        squareCoord = IsRotated ? String.fromCharCode(72-jj,49+ii) : String.fromCharCode(jj+65,56-ii);
3320        squareTitle = squareCoord;
3321        if (boardTitle[jj][ii] !== '') { squareTitle += ': ' + boardTitle[jj][ii]; }
3322        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>';
3323      }
3324      text += '</TR>';
3325    }
3326    text += '</TABLE>';
3327
3328    theObj.innerHTML = text;
3329  }
3330
3331  if (theObj = document.getElementById("boardTable")) {
3332    tableSize = theObj.offsetWidth;
3333    if (tableSize > 0) { // coping with browser always returning 0 to offsetWidth
3334      theObj.style.height = tableSize + "px";
3335    }
3336  }
3337
3338  // control buttons
3339
3340  if (theObj = document.getElementById("GameButtons")) {
3341    var numButtons = 5;
3342    var spaceSize = 3;
3343    var buttonSize = (tableSize - spaceSize*(numButtons - 1)) / numButtons;
3344    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="';
3345    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
3346    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="';
3347    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
3348    text += '"; CLASS="buttonControl" TITLE="move backward" ID="btnMoveBackward1" onClick="clickedBbtn(this,event);" ONFOCUS="this.blur();"></TD><TD CLASS="buttonControlSpace" WIDTH="' + spaceSize + '"></TD><TD>';
3349    text += '<INPUT ID="autoplayButton" TYPE="BUTTON" VALUE=' + (isAutoPlayOn ? "=" : "\u2192") + ' STYLE="';
3350    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
3351    text += isAutoPlayOn ? '"; CLASS="buttonControlStop" TITLE="toggle autoplay (stop)" ' : '"; CLASS="buttonControlPlay" TITLE="toggle autoplay (start)" ';
3352    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="';
3353    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
3354    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="';
3355    if (buttonSize > 0) { text += 'width: ' + buttonSize + 'px;'; }
3356    text += '"; CLASS="buttonControl" TITLE="go to game end" ID="btnGoToEnd" onClick="clickedBbtn(this,event);" ONFOCUS="this.blur();"></TD></TR></TABLE></FORM>';
3357
3358    theObj.innerHTML = text;
3359  }
3360
3361  // game selector
3362
3363  if (theObj = document.getElementById("GameSelector")) {
3364    if (firstStart) { textSelectOptions=''; }
3365    if (numberOfGames < 2) {
3366      while (theObj.firstChild) { theObj.removeChild(theObj.firstChild); }
3367      textSelectOptions = '';
3368    } else {
3369      if (textSelectOptions === '') {
3370        if (gameSelectorNum) { gameSelectorNumLenght = Math.floor(Math.log(numberOfGames)/Math.log(10)) + 1; }
3371        text = '<FORM NAME="GameSel" STYLE="display:inline;"><SELECT ID="GameSelSelect" NAME="GameSelSelect" STYLE="';
3372        if (tableSize > 0) { text += 'width: ' + tableSize + 'px; '; }
3373        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>';
3374
3375        var blanks = ''; for (ii=0; ii<32; ii++) { blanks += ' '; }
3376        var headDisplay = (gameSelectorNum ? blanks.substring(0, gameSelectorNumLenght) + '  ' : '') + gameSelectorHead;
3377        text += headDisplay.replace(/ /g, '&nbsp;');
3378
3379        for (ii=0; ii<numberOfGames; ii++) {
3380          textSelectOptions += '<OPTION CLASS="optionSelectControl" value=' + ii + '>';
3381          textSO = '';
3382          if (gameSelectorNum) {
3383            numText = ' ' + (ii+1);
3384            textSO += blanks.substr(0, gameSelectorNumLenght - (numText.length - 1)) + numText + ' ';
3385          }
3386          if (gameSelectorChEvent > 0) {
3387            textSO += ' ' + gameEvent[ii].substring(0, gameSelectorChEvent) + blanks.substr(0, gameSelectorChEvent - gameEvent[ii].length) + ' ';
3388          }
3389          if (gameSelectorChSite > 0) {
3390            textSO += ' ' + gameSite[ii].substring(0, gameSelectorChSite) + blanks.substr(0, gameSelectorChSite - gameSite[ii].length) + ' ';
3391          }
3392          if (gameSelectorChRound > 0) {
3393            textSO += ' ' + blanks.substr(0, gameSelectorChRound - gameRound[ii].length) + gameRound[ii].substring(0, gameSelectorChRound) + ' ';
3394          }
3395          if (gameSelectorChWhite > 0) {
3396            textSO += ' ' + gameWhite[ii].substring(0, gameSelectorChWhite) + blanks.substr(0, gameSelectorChWhite - gameWhite[ii].length) + ' ';
3397          }
3398          if (gameSelectorChBlack > 0) {
3399            textSO += ' ' + gameBlack[ii].substring(0, gameSelectorChBlack) + blanks.substr(0, gameSelectorChBlack - gameBlack[ii].length) + ' ';
3400          }
3401          if (gameSelectorChResult > 0) {
3402            textSO += ' ' + gameResult[ii].substring(0, gameSelectorChResult) + blanks.substr(0, gameSelectorChResult - gameResult[ii].length) + ' ';
3403          }
3404          if (gameSelectorChDate > 0) {
3405            textSO += ' ' + gameDate[ii].substring(0, gameSelectorChDate) + blanks.substr(0, gameSelectorChDate - gameDate[ii].length) + ' ';
3406          }
3407          textSelectOptions += textSO.replace(/ /g, '&nbsp;');
3408        }
3409        text += textSelectOptions.replace(/&(amp|lt|gt);/g, '&amp;$1;') + '</SELECT></FORM>'; // see function simpleHtmlentities()
3410        theObj.innerHTML = text;
3411      }
3412    }
3413  }
3414
3415  // game event
3416
3417  if (theObj = document.getElementById("GameEvent")) { theObj.innerHTML = gameEvent[currentGame]; }
3418
3419  // game round
3420
3421  if (theObj = document.getElementById("GameRound")) { theObj.innerHTML = gameRound[currentGame]; }
3422
3423  // game site
3424
3425  if (theObj = document.getElementById("GameSite")) { theObj.innerHTML = gameSite[currentGame]; }
3426
3427  // game date
3428
3429  if (theObj = document.getElementById("GameDate")) {
3430    theObj.innerHTML = gameDate[currentGame];
3431    theObj.style.whiteSpace = "nowrap";
3432  }
3433
3434  // game white
3435
3436  if (theObj = document.getElementById("GameWhite")) { theObj.innerHTML = gameWhite[currentGame]; }
3437
3438  // game black
3439
3440  if (theObj = document.getElementById("GameBlack")) { theObj.innerHTML = gameBlack[currentGame]; }
3441
3442  // game result
3443
3444  if (theObj = document.getElementById("GameResult")) {
3445    theObj.innerHTML = gameResult[currentGame];
3446    theObj.style.whiteSpace = "nowrap";
3447  }
3448
3449  // game text
3450
3451  if (theObj = document.getElementById("GameText")) {
3452    variationTextDepth = -1;
3453    text = '<SPAN ID="ShowPgnText">' + variationTextFromId(0); + '</SPAN>';
3454    theObj.innerHTML = text;
3455  }
3456
3457  setB1C1F1G1boardShortcuts(); // depend on presence of comments
3458
3459  // game searchbox
3460
3461  if ((theObj = document.getElementById("GameSearch")) && firstStart) {
3462    if (numberOfGames < 2) {
3463      while (theObj.firstChild) { theObj.removeChild(theObj.firstChild); }
3464    } else {
3465      text = '<FORM ID="searchPgnForm" STYLE="display: inline;" ACTION="javascript:searchPgnGameForm();"><INPUT ID="searchPgnButton" CLASS="searchPgnButton" STYLE="display: inline; ';
3466      if (tableSize > 0) { text += 'width: ' + (tableSize/4) + 'px; '; }
3467      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;';
3468      if (tableSize > 0) { text += 'width: ' + (3*tableSize/4) + 'px; '; }
3469      text += '" ONFOCUS="disableShortcutKeysAndStoreStatus();" ONBLUR="restoreShortcutKeysStatus();"></FORM>';
3470      theObj.innerHTML = text;
3471      theObj = document.getElementById('searchPgnExpression');
3472      if (theObj) { theObj.value = lastSearchPgnExpression; }
3473    }
3474  }
3475}
3476
3477function startButton(e) {
3478  if (e.shiftKey) {
3479    GoToMove(StartPlyVar[CurrentVar] + (CurrentPly <= StartPlyVar[CurrentVar] + 1 ? 0 : 1));
3480  } else { GoToMove(StartPlyVar[0], 0); }
3481}
3482
3483function backButton(e) {
3484  if (e.shiftKey) { GoToMove(StartPlyVar[CurrentVar]); }
3485  else { GoToMove(CurrentPly - 1); }
3486}
3487
3488function forwardButton(e) {
3489  if (e.shiftKey) { if (!goToNextVariationSibling()) { GoToMove(CurrentPly + 1); } }
3490  else { GoToMove(CurrentPly + 1); }
3491}
3492
3493function endButton(e) {
3494  if (e.shiftKey) {
3495    if (CurrentPly === StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]) { goToFirstChild(); }
3496    else { GoToMove(StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]); }
3497  } else { GoToMove(StartPlyVar[0] + PlyNumberVar[0], 0); }
3498}
3499
3500function clickedBbtn(t,e) {
3501  switch (t.id) {
3502    case "startButton":
3503      startButton(e);
3504      break;
3505    case "backButton":
3506      backButton(e);
3507      break;
3508    case "autoplayButton":
3509      if (e.shiftKey) { goToNextVariationSibling(); }
3510      else { SwitchAutoPlay(); }
3511      break;
3512    case "forwardButton":
3513      forwardButton(e);
3514      break;
3515    case "endButton":
3516      endButton(e);
3517      break;
3518    default:
3519      break;
3520  }
3521}
3522
3523var basicNAGs = /^[\?!+#\s]+(\s|$)/;
3524function strippedMoveComment(plyNum, varId, addHtmlTags) {
3525  if (typeof(addHtmlTags) == "undefined") {
3526    addHtmlTags = false;
3527    if (typeof(varId) == "undefined") { varId = CurrentVar; }
3528  }
3529  if (!MoveCommentsVar[varId][plyNum]) { return ""; }
3530  return fixCommentForDisplay(MoveCommentsVar[varId][plyNum]).replace(pgn4webVariationRegExpGlobal, function (m) { return variationTextFromTag(m, addHtmlTags); }).replace(/\[%[^\]]*\]\s*/g,'').replace(plyNum === StartPlyVar[varId] ? '' : basicNAGs, '').replace(/^\s+$/,'');
3531}
3532
3533function basicNAGsMoveComment(plyNum, varId) {
3534  if (typeof(varId) == "undefined") { varId = CurrentVar; }
3535  if (!MoveCommentsVar[varId][plyNum]) { return ""; }
3536  var thisBasicNAGs = MoveCommentsVar[varId][plyNum].replace(/\[%[^\]]*\]\s*/g,'').match(basicNAGs, '');
3537  return thisBasicNAGs ? thisBasicNAGs[0].replace(/\s+(?!class=)/gi,'') : '';
3538}
3539
3540function variationTextFromTag(variationTag, addHtmlTags) {
3541  if (typeof(addHtmlTags) == "undefined") { addHtmlTags = false; }
3542  var varId = variationTag.replace(pgn4webVariationRegExp, "$1");
3543  if (isNaN(varId)) {
3544    myAlert("error: issue parsing variation tag " + variationTag + " in game " + (currentGame+1), true);
3545    return "";
3546  }
3547  var text = variationTextFromId(varId);
3548  if (text) {
3549    if (addHtmlTags) { text = '</SPAN>' + text + '<SPAN CLASS="comment">'; }
3550  } else { text = ''; }
3551  return text;
3552}
3553
3554var variationTextDepth, printedComment, printedVariation;
3555function variationTextFromId(varId) {
3556  var punctChars = ",.;:!?", thisComment;
3557
3558  if (isNaN(varId) || varId < 0 || varId >= numberOfVars || typeof(StartPlyVar[varId]) == "undefined" || typeof(PlyNumberVar[varId]) == "undefined") {
3559    myAlert("error: issue parsing variation id " + varId + " in game " + (currentGame+1), true);
3560    return "";
3561  }
3562  var text = ++variationTextDepth ? ('<SPAN CLASS="variation">' + (printedVariation ? ' ' : '') + (variationTextDepth > 1 ? '(' : '[')) + '</SPAN>' : '';
3563  printedVariation = false;
3564  for (var ii = StartPlyVar[varId]; ii < StartPlyVar[varId] + PlyNumberVar[varId]; ii++) {
3565    printedComment = false;
3566    if (commentsIntoMoveText && (thisComment = strippedMoveComment(ii, varId, true))) {
3567      if (commentsOnSeparateLines && variationTextDepth === 0 && ii > StartPlyVar[varId]) {
3568        text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
3569      }
3570      if (printedVariation) { if (punctChars.indexOf(thisComment.charAt(0)) == -1) { text += '<SPAN CLASS="variation"> </SPAN>'; } }
3571      else { printedVariation = variationTextDepth > 0; }
3572      text += '<SPAN CLASS="comment">' + thisComment + '</SPAN>';
3573      if (commentsOnSeparateLines && variationTextDepth === 0) {
3574        text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
3575      }
3576      printedComment = true;
3577    }
3578    if (printedComment || printedVariation) { text += '<SPAN CLASS="variation"> </SPAN>'; }
3579    printedVariation = true;
3580
3581    text += printMoveText(ii, varId, (variationTextDepth > 0), ((printedComment) || (ii == StartPlyVar[varId])), true);
3582  }
3583  if (commentsIntoMoveText && (thisComment = strippedMoveComment(StartPlyVar[varId] + PlyNumberVar[varId], varId, true))) {
3584    if (commentsOnSeparateLines && variationTextDepth === 0) {
3585      text += '<DIV CLASS="comment" STYLE="line-height: 33%;">&nbsp;</DIV>';
3586    }
3587    if (printedVariation && (punctChars.indexOf(thisComment.charAt(0)) == -1)) { text += '<SPAN CLASS="comment notranslate"> </SPAN>'; }
3588    text += '<SPAN CLASS="comment">' + thisComment + '</SPAN>';
3589    printedComment = true;
3590  }
3591  text += variationTextDepth-- ? ('<SPAN CLASS="variation">' + (variationTextDepth ? ')' : ']') + '</SPAN>') : '';
3592  printedVariation = true;
3593  return text;
3594}
3595
3596function printMoveText(thisPly, thisVar, isVar, hasLeadingNum, hasId) {
3597  if (typeof(thisVar) == "undefined") { thisVar = CurrentVar; }
3598  if (typeof(thisPly) == "undefined") { thisPly = CurrentPly; }
3599  var text = '';
3600
3601  if (thisVar >= numberOfVars || thisPly < StartPlyVar[thisVar] || thisPly > StartPlyVar[thisVar] + PlyNumberVar[thisVar]) {
3602    return text;
3603  }
3604
3605  var moveCount = Math.floor(thisPly/2)+1;
3606  if (thisPly%2 === 0) {
3607    text += '<SPAN CLASS="' + (isVar ? 'variation' : 'move') + ' notranslate">' + moveCount + '.&nbsp;</SPAN>';
3608  } else {
3609    if (hasLeadingNum) {
3610      text += '<SPAN CLASS="' + (isVar ? 'variation' : 'move') + ' notranslate">' + moveCount + '...&nbsp;</SPAN>';
3611    }
3612  }
3613  var jj = thisPly+1;
3614  text += '<A HREF="javascript:void(0);" ONCLICK="GoToMove(' + jj + ', ' + thisVar + ');" CLASS="' + (isVar ? 'variation' : 'move') + ' notranslate" ';
3615  if (hasId) { text += 'ID="Var' + thisVar + 'Mv' + jj + '" '; }
3616  text += 'ONFOCUS="this.blur();">' + MovesVar[thisVar][thisPly];
3617  if (commentsIntoMoveText) { text += basicNAGsMoveComment(jj, thisVar); }
3618  text += '</A>';
3619
3620  return text;
3621}
3622
3623
3624// undocumented API to autoscroll gane text to show current move
3625// enable with "enableAutoScrollToCurrentMove(objId)", with objId as the game text container object id
3626// add onresize="autoScrollToCurrentMoveIfEnabled" to the body tag for autoscrolling on page resize
3627
3628function enableAutoScrollToCurrentMove(objId) { autoScrollToCurrentMove_objId = objId; }
3629function disableAutoScrollToCurrentMove() { autoScrollToCurrentMove_objId = ""; }
3630function toggleAutoScrollToCurrentMove(objId) { autoScrollToCurrentMove_objId = autoScrollToCurrentMove_objId ? "" : objId; }
3631
3632var autoScrollToCurrentMove_objId = "";
3633function autoScrollToCurrentMoveIfEnabled() { autoScrollToCurrentMove(autoScrollToCurrentMove_objId); }
3634
3635function objOffsetVeryTop(obj) {
3636  for (var offset = obj.offsetTop; obj = obj.offsetParent; /* */) { offset += obj.offsetTop + obj.clientTop; }
3637  return offset;
3638}
3639
3640function autoScrollToCurrentMove(objId) {
3641  if (!objId) { return; }
3642  var theContainerObj = document.getElementById(objId);
3643  if (theContainerObj) {
3644    if (CurrentPly == StartPly) { theContainerObj.scrollTop = 0; }
3645    else {
3646      var theMoveObj = document.getElementById('Var' + CurrentVar + 'Mv' + CurrentPly);
3647      if (theMoveObj) {
3648        var theContainerObjOffsetVeryTop = objOffsetVeryTop(theContainerObj);
3649        var theMoveObjOffsetVeryTop = objOffsetVeryTop(theMoveObj);
3650        if ((theMoveObjOffsetVeryTop + theMoveObj.offsetHeight > theContainerObjOffsetVeryTop + theContainerObj.scrollTop + theContainerObj.clientHeight) || (theMoveObjOffsetVeryTop < theContainerObjOffsetVeryTop + theContainerObj.scrollTop)) {
3651          theContainerObj.scrollTop = theMoveObjOffsetVeryTop - theContainerObjOffsetVeryTop;
3652        }
3653      }
3654    }
3655  }
3656}
3657
3658
3659function FlipBoard() {
3660  var oldHighlightOption = highlightOption;
3661  if (oldHighlightOption) { SetHighlight(false); }
3662  IsRotated = !IsRotated;
3663  PrintHTML();
3664  RefreshBoard();
3665  if (oldHighlightOption) { SetHighlight(true); }
3666}
3667
3668function RefreshBoard() {
3669  for (var jj = 0; jj < 8; ++jj) {
3670    for (var ii = 0; ii < 8; ++ii) {
3671      if (Board[jj][ii] === 0) { SetImage(jj, ii, ClearImg.src); }
3672    }
3673  }
3674  for (jj = 0; jj < 2; ++jj) {
3675    for (ii = 0; ii < 16; ++ii) {
3676      if (PieceType[jj][ii] > 0) {
3677        SetImage(PieceCol[jj][ii], PieceRow[jj][ii], PieceImg[jj][PieceType[jj][ii]].src);
3678      }
3679    }
3680  }
3681}
3682
3683function SetAutoPlay(vv) {
3684  isAutoPlayOn = vv;
3685
3686  if (AutoPlayInterval) { clearTimeout(AutoPlayInterval); AutoPlayInterval = null; }
3687
3688  if (isAutoPlayOn) {
3689    if (document.GameButtonsForm) {
3690      if (document.GameButtonsForm.AutoPlay) {
3691        document.GameButtonsForm.AutoPlay.value = "||";
3692        document.GameButtonsForm.AutoPlay.title = "toggle autoplay (stop)";
3693        document.GameButtonsForm.AutoPlay.className = "buttonControlStop";
3694      }
3695    }
3696    if (CurrentPly < StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]) { AutoPlayInterval=setTimeout("MoveForward(1)", Delay); }
3697    else {
3698      if (autoplayNextGame && (CurrentVar === 0)) { AutoPlayInterval=setTimeout("AutoplayNextGame()", Delay); }
3699      else { SetAutoPlay(false); }
3700    }
3701  } else {
3702    if (document.GameButtonsForm) {
3703      if (document.GameButtonsForm.AutoPlay) {
3704        document.GameButtonsForm.AutoPlay.value = "\u2192";
3705        document.GameButtonsForm.AutoPlay.title = "toggle autoplay (start)";
3706        document.GameButtonsForm.AutoPlay.className = "buttonControlPlay";
3707      }
3708    }
3709  }
3710}
3711
3712
3713var minAutoplayDelay = 500;
3714var maxAutoplayDelay = 300000;
3715function setCustomAutoplayDelay() {
3716  var newDelaySec = prompt("Enter custom autoplay delay, in seconds, between " + (minAutoplayDelay/1000) + " and " + (maxAutoplayDelay/1000) + ":", Math.floor(Delay / 100) / 10);
3717  if (!isNaN(newDelaySec = parseInt(newDelaySec, 10))) {
3718    SetAutoplayDelayAndStart(newDelaySec * 1000);
3719  }
3720}
3721
3722function SetAutoplayDelay(vv) {
3723  if (isNaN(vv = parseInt(vv, 10))) { return; }
3724  Delay = Math.min(Math.max(vv, minAutoplayDelay), maxAutoplayDelay);
3725}
3726
3727function SetAutoplayDelayAndStart(vv) {
3728  MoveForward(1);
3729  SetAutoplayDelay(vv);
3730  SetAutoPlay(true);
3731}
3732
3733function SetLiveBroadcast(delay, alertFlag, demoFlag, stepFlag, endlessFlag) {
3734  LiveBroadcastDelay = delay; // zero delay: no live broadcast
3735  LiveBroadcastAlert = (alertFlag === true);
3736  LiveBroadcastDemo = (demoFlag === true);
3737  LiveBroadcastSteppingMode = (stepFlag === true);
3738  LiveBroadcastEndlessMode = (endlessFlag === true);
3739  setG7A6B6H7boardShortcuts();
3740}
3741
3742function SetImage(col, row, image) {
3743  var trow = IsRotated ? row : 7 - row;
3744  var tcol = IsRotated ? 7 - col : col;
3745  var theObj = document.getElementById('img_' + 'tcol' + tcol + 'trow' + trow);
3746  if ((theObj) && (theObj.src != image)) { theObj.src = image; }
3747}
3748
3749function SetImagePath(path) {
3750  ImagePath = path;
3751}
3752
3753function SwitchAutoPlay() {
3754  if (!isAutoPlayOn) { MoveForward(1); }
3755  SetAutoPlay(!isAutoPlayOn);
3756}
3757
3758function StoreMove(thisPly) {
3759
3760  HistVar[thisPly+1] = CurrentVar;
3761
3762  if (HistNull[thisPly] = mvIsNull) { return; }
3763
3764  // "square from" history
3765  HistPieceId[0][thisPly] = mvPieceId;
3766  HistCol[0][thisPly] = PieceCol[MoveColor][mvPieceId];
3767  HistRow[0][thisPly] = PieceRow[MoveColor][mvPieceId];
3768  HistType[0][thisPly] = PieceType[MoveColor][mvPieceId];
3769
3770  // "square to" history
3771  HistCol[2][thisPly] = mvToCol;
3772  HistRow[2][thisPly] = mvToRow;
3773
3774  if (mvIsCastling) {
3775    HistPieceId[1][thisPly] = castleRook;
3776    HistCol[1][thisPly] = PieceCol[MoveColor][castleRook];
3777    HistRow[1][thisPly] = PieceRow[MoveColor][castleRook];
3778    HistType[1][thisPly] = PieceType[MoveColor][castleRook];
3779  } else if (mvCapturedId >= 0) {
3780    HistPieceId[1][thisPly] = mvCapturedId+16;
3781    HistCol[1][thisPly] = PieceCol[1-MoveColor][mvCapturedId];
3782    HistRow[1][thisPly] = PieceRow[1-MoveColor][mvCapturedId];
3783    HistType[1][thisPly] = PieceType[1-MoveColor][mvCapturedId];
3784  } else {
3785    HistPieceId[1][thisPly] = -1;
3786  }
3787
3788  // update "square from" and captured square (not "square to" for en-passant)
3789  Board[PieceCol[MoveColor][mvPieceId]][PieceRow[MoveColor][mvPieceId]] = 0;
3790
3791  // mark captured piece
3792  if (mvCapturedId >= 0) {
3793    PieceType[1-MoveColor][mvCapturedId] = -1;
3794    PieceMoveCounter[1-MoveColor][mvCapturedId]++;
3795    Board[PieceCol[1-MoveColor][mvCapturedId]][PieceRow[1-MoveColor][mvCapturedId]] = 0;
3796  }
3797
3798  // update piece arrays: promotion changes piece type
3799  PieceType[MoveColor][mvPieceId] = mvPieceOnTo;
3800  PieceMoveCounter[MoveColor][mvPieceId]++;
3801  PieceCol[MoveColor][mvPieceId] = mvToCol;
3802  PieceRow[MoveColor][mvPieceId] = mvToRow;
3803  if (mvIsCastling) {
3804    PieceMoveCounter[MoveColor][castleRook]++;
3805    PieceCol[MoveColor][castleRook] = mvToCol == 2 ? 3 : 5;
3806    PieceRow[MoveColor][castleRook] = mvToRow;
3807  }
3808
3809  // update board
3810  Board[mvToCol][mvToRow] = PieceType[MoveColor][mvPieceId]*(1-2*MoveColor);
3811  if (mvIsCastling) {
3812    Board[PieceCol[MoveColor][castleRook]][PieceRow[MoveColor][castleRook]] = PieceType[MoveColor][castleRook]*(1-2*MoveColor);
3813  }
3814  return;
3815}
3816
3817function UndoMove(thisPly) {
3818
3819  if (HistNull[thisPly]) { return; }
3820
3821  // moved piece back to original square
3822  var chgPiece = HistPieceId[0][thisPly];
3823  Board[PieceCol[MoveColor][chgPiece]][PieceRow[MoveColor][chgPiece]] = 0;
3824
3825  Board[HistCol[0][thisPly]][HistRow[0][thisPly]] = HistType[0][thisPly] * (1-2*MoveColor);
3826  PieceType[MoveColor][chgPiece] = HistType[0][thisPly];
3827  PieceCol[MoveColor][chgPiece] = HistCol[0][thisPly];
3828  PieceRow[MoveColor][chgPiece] = HistRow[0][thisPly];
3829  PieceMoveCounter[MoveColor][chgPiece]--;
3830
3831  // castling: rook back to original square
3832  chgPiece = HistPieceId[1][thisPly];
3833  if ((chgPiece >= 0) && (chgPiece < 16)) {
3834    Board[PieceCol[MoveColor][chgPiece]][PieceRow[MoveColor][chgPiece]] = 0;
3835    Board[HistCol[1][thisPly]][HistRow[1][thisPly]] = HistType[1][thisPly] * (1-2*MoveColor);
3836    PieceType[MoveColor][chgPiece] = HistType[1][thisPly];
3837    PieceCol[MoveColor][chgPiece] = HistCol[1][thisPly];
3838    PieceRow[MoveColor][chgPiece] = HistRow[1][thisPly];
3839    PieceMoveCounter[MoveColor][chgPiece]--;
3840  }
3841
3842  // capture: captured piece back to original square
3843  chgPiece -= 16;
3844  if ((chgPiece >= 0) && (chgPiece < 16)) {
3845    Board[PieceCol[1-MoveColor][chgPiece]][PieceRow[1-MoveColor][chgPiece]] = 0;
3846    Board[HistCol[1][thisPly]][HistRow[1][thisPly]] = HistType[1][thisPly] * (2*MoveColor-1);
3847    PieceType[1-MoveColor][chgPiece] = HistType[1][thisPly];
3848    PieceCol[1-MoveColor][chgPiece] = HistCol[1][thisPly];
3849    PieceRow[1-MoveColor][chgPiece] = HistRow[1][thisPly];
3850    PieceMoveCounter[1-MoveColor][chgPiece]--;
3851  }
3852}
3853
3854function Color(nn) {
3855  if (nn < 0) { return 1; }
3856  if (nn > 0) { return 0; }
3857  return 2;
3858}
3859
3860function sign(nn) {
3861  if (nn > 0) { return  1; }
3862  if (nn < 0) { return -1; }
3863  return 0;
3864}
3865
3866function SquareOnBoard(col, row) {
3867  return col >= 0 && col <= 7 && row >= 0 && row <= 7;
3868}
3869
3870var pgn4webMaxTouches = 0;
3871var pgn4webOngoingTouches = new Array();
3872function pgn4webOngoingTouchIndexById(needle) {
3873  var id;
3874  for (var ii = 0; ii < pgn4webOngoingTouches.length; ii++) {
3875    id = pgn4webOngoingTouches[ii].identifier;
3876    if (pgn4webOngoingTouches[ii].identifier === needle) { return ii; }
3877  }
3878  return -1;
3879}
3880
3881function pgn4web_handleTouchStart(e) {
3882  e.stopPropagation();
3883  for (var ii = 0; ii < e.changedTouches.length; ii++) {
3884    pgn4webMaxTouches++;
3885    pgn4webOngoingTouches.push({ identifier: e.changedTouches[ii].identifier, clientX: e.changedTouches[ii].clientX, clientY: e.changedTouches[ii].clientY });
3886  }
3887}
3888
3889function pgn4web_handleTouchMove(e) {
3890  e.stopPropagation();
3891  e.preventDefault();
3892}
3893
3894function pgn4web_handleTouchEnd(e) {
3895  e.stopPropagation();
3896  var jj;
3897  for (var ii = 0; ii < e.changedTouches.length; ii++) {
3898    if ((jj = pgn4webOngoingTouchIndexById(e.changedTouches[ii].identifier)) != -1) {
3899      if (pgn4webOngoingTouches.length == 1) {
3900        customFunctionOnTouch(e.changedTouches[ii].clientX - pgn4webOngoingTouches[jj].clientX, e.changedTouches[ii].clientY - pgn4webOngoingTouches[jj].clientY);
3901        pgn4webMaxTouches = 0;
3902      }
3903      pgn4webOngoingTouches.splice(jj, 1);
3904    }
3905  }
3906  clearSelectedText();
3907}
3908
3909function pgn4web_handleTouchCancel(e) {
3910  e.stopPropagation();
3911  var jj;
3912  for (var ii = 0; ii < e.changedTouches.length; ii++) {
3913    if ((jj = pgn4webOngoingTouchIndexById(e.changedTouches[ii].identifier)) != -1) {
3914      pgn4webOngoingTouches.splice(jj, 1);
3915      if (pgn4webOngoingTouches.length === 0) { pgn4webMaxTouches = 0; }
3916    }
3917  }
3918  clearSelectedText();
3919}
3920
3921function pgn4web_initTouchEvents() {
3922  var theObj = document.getElementById("GameBoard");
3923  if (theObj && touchEventEnabled) {
3924    simpleAddEvent(theObj, "touchstart", pgn4web_handleTouchStart);
3925    simpleAddEvent(theObj, "touchmove", pgn4web_handleTouchMove);
3926    simpleAddEvent(theObj, "touchend", pgn4web_handleTouchEnd);
3927    simpleAddEvent(theObj, "touchleave", pgn4web_handleTouchEnd);
3928    simpleAddEvent(theObj, "touchcancel", pgn4web_handleTouchCancel);
3929  }
3930}
3931
3932var waitForDoubleLeftTouchTimer = null;
3933function customFunctionOnTouch(deltaX, deltaY) {
3934  if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 13) { return; }
3935  if (Math.abs(deltaY) > 1.5 * Math.abs(deltaX)) { // vertical up or down
3936    if (numberOfGames > 1) {
3937      if ((currentGame === 0) && (deltaY < 0)) { Init(numberOfGames - 1); }
3938      else if ((currentGame === numberOfGames - 1) && (deltaY > 0)) { Init(0); }
3939      else { Init(currentGame + sign(deltaY)); }
3940    }
3941  } else if (Math.abs(deltaX) > 1.5 * Math.abs(deltaY)) {
3942    if (deltaX > 0) { // horizontal right
3943      if (isAutoPlayOn) { GoToMove(StartPlyVar[CurrentVar] + PlyNumberVar[CurrentVar]); }
3944      else { SwitchAutoPlay(); }
3945    } else { // horizontal left
3946      if (isAutoPlayOn && !waitForDoubleLeftTouchTimer) { SwitchAutoPlay(); }
3947      else {
3948        if (waitForDoubleLeftTouchTimer) {
3949          clearTimeout(waitForDoubleLeftTouchTimer);
3950          waitForDoubleLeftTouchTimer = null;
3951        }
3952        if ((LiveBroadcastDelay > 0) && (CurrentVar === 0) && (CurrentPly === StartPly + PlyNumber)) {
3953          waitForDoubleLeftTouchTimer = setTimeout("waitForDoubleLeftTouchTimer = null;", 900);
3954          replayPreviousMoves(6);
3955        } else { GoToMove(StartPlyVar[CurrentVar] + (((CurrentPly <= StartPlyVar[CurrentVar] + 1) || (CurrentVar === 0)) ? 0 : 1)); }
3956      }
3957    }
3958  }
3959}
3960
3961var touchGestures_helpActions = [ "chessboard top-down swipe", "chessboard bottom-up swipe", "chessboard left-right swipe", "chessboard right-left swipe" ];
3962var 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" ];
3963
3964var touchEventEnabled = true;
3965function SetTouchEventEnabled(onOff) {
3966  touchEventEnabled = onOff;
3967}
3968
3969function clearSelectedText() {
3970  if (window.getSelection) {
3971    if (window.getSelection().empty) { window.getSelection().empty(); }
3972    else if (window.getSelection().removeAllRanges) { window.getSelection().removeAllRanges(); }
3973  } else if (document.selection && document.selection.empty) { document.selection.empty(); }
3974}
3975
3976function simpleHtmlentities(text) {
3977  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3978}
3979
3980function simpleHtmlentitiesDecode(text) {
3981  return text.replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
3982}
3983
3984