]> git.sesse.net Git - remoteglot/blob - www/js/chessboard-0.3.0.js
Remove the spare piece functionality we do not use.
[remoteglot] / www / js / chessboard-0.3.0.js
1 /*!
2  * chessboard.js v0.3.0+asn
3  *
4  * Copyright 2013 Chris Oakman
5  * Portions copyright 2022 Steinar H. Gunderson
6  * Released under the MIT license
7  * http://chessboardjs.com/license
8  *
9  * Date: 10 Aug 2013
10  */
11
12 // start anonymous scope
13 ;(function() {
14 'use strict';
15
16 //------------------------------------------------------------------------------
17 // Chess Util Functions
18 //------------------------------------------------------------------------------
19 var COLUMNS = 'abcdefgh'.split('');
20
21 function validMove(move) {
22   // move should be a string
23   if (typeof move !== 'string') return false;
24
25   // move should be in the form of "e2-e4", "f6-d5"
26   var tmp = move.split('-');
27   if (tmp.length !== 2) return false;
28
29   return (validSquare(tmp[0]) === true && validSquare(tmp[1]) === true);
30 }
31
32 function validSquare(square) {
33   if (typeof square !== 'string') return false;
34   return (square.search(/^[a-h][1-8]$/) !== -1);
35 }
36
37 function validPieceCode(code) {
38   if (typeof code !== 'string') return false;
39   return (code.search(/^[bw][KQRNBP]$/) !== -1);
40 }
41
42 // TODO: this whole function could probably be replaced with a single regex
43 function validFen(fen) {
44   if (typeof fen !== 'string') return false;
45
46   // cut off any move, castling, etc info from the end
47   // we're only interested in position information
48   fen = fen.replace(/ .+$/, '');
49
50   // FEN should be 8 sections separated by slashes
51   var chunks = fen.split('/');
52   if (chunks.length !== 8) return false;
53
54   // check the piece sections
55   for (var i = 0; i < 8; i++) {
56     if (chunks[i] === '' ||
57         chunks[i].length > 8 ||
58         chunks[i].search(/[^kqrbnpKQRNBP1-8]/) !== -1) {
59       return false;
60     }
61   }
62
63   return true;
64 }
65
66 function validPositionObject(pos) {
67   if (typeof pos !== 'object') return false;
68
69   for (var i in pos) {
70     if (pos.hasOwnProperty(i) !== true) continue;
71
72     if (validSquare(i) !== true || validPieceCode(pos[i]) !== true) {
73       return false;
74     }
75   }
76
77   return true;
78 }
79
80 // convert FEN piece code to bP, wK, etc
81 function fenToPieceCode(piece) {
82   // black piece
83   if (piece.toLowerCase() === piece) {
84     return 'b' + piece.toUpperCase();
85   }
86
87   // white piece
88   return 'w' + piece.toUpperCase();
89 }
90
91 // convert bP, wK, etc code to FEN structure
92 function pieceCodeToFen(piece) {
93   var tmp = piece.split('');
94
95   // white piece
96   if (tmp[0] === 'w') {
97     return tmp[1].toUpperCase();
98   }
99
100   // black piece
101   return tmp[1].toLowerCase();
102 }
103
104 // convert FEN string to position object
105 // returns false if the FEN string is invalid
106 function fenToObj(fen) {
107   if (validFen(fen) !== true) {
108     return false;
109   }
110
111   // cut off any move, castling, etc info from the end
112   // we're only interested in position information
113   fen = fen.replace(/ .+$/, '');
114
115   var rows = fen.split('/');
116   var position = {};
117
118   var currentRow = 8;
119   for (var i = 0; i < 8; i++) {
120     var row = rows[i].split('');
121     var colIndex = 0;
122
123     // loop through each character in the FEN section
124     for (var j = 0; j < row.length; j++) {
125       // number / empty squares
126       if (row[j].search(/[1-8]/) !== -1) {
127         var emptySquares = parseInt(row[j], 10);
128         colIndex += emptySquares;
129       }
130       // piece
131       else {
132         var square = COLUMNS[colIndex] + currentRow;
133         position[square] = fenToPieceCode(row[j]);
134         colIndex++;
135       }
136     }
137
138     currentRow--;
139   }
140
141   return position;
142 }
143
144 // position object to FEN string
145 // returns false if the obj is not a valid position object
146 function objToFen(obj) {
147   if (validPositionObject(obj) !== true) {
148     return false;
149   }
150
151   var fen = '';
152
153   var currentRow = 8;
154   for (var i = 0; i < 8; i++) {
155     for (var j = 0; j < 8; j++) {
156       var square = COLUMNS[j] + currentRow;
157
158       // piece exists
159       if (obj.hasOwnProperty(square) === true) {
160         fen += pieceCodeToFen(obj[square]);
161       }
162
163       // empty space
164       else {
165         fen += '1';
166       }
167     }
168
169     if (i !== 7) {
170       fen += '/';
171     }
172
173     currentRow--;
174   }
175
176   // squeeze the numbers together
177   // haha, I love this solution...
178   fen = fen.replace(/11111111/g, '8');
179   fen = fen.replace(/1111111/g, '7');
180   fen = fen.replace(/111111/g, '6');
181   fen = fen.replace(/11111/g, '5');
182   fen = fen.replace(/1111/g, '4');
183   fen = fen.replace(/111/g, '3');
184   fen = fen.replace(/11/g, '2');
185
186   return fen;
187 }
188
189 /** @struct */
190 var cfg;
191
192 /** @constructor */
193 window.ChessBoard = function(containerElOrId, cfg) {
194 'use strict';
195
196 cfg = cfg || {};
197
198 //------------------------------------------------------------------------------
199 // Constants
200 //------------------------------------------------------------------------------
201
202 var MINIMUM_JQUERY_VERSION = '1.7.0',
203   START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR',
204   START_POSITION = fenToObj(START_FEN);
205
206 // use unique class names to prevent clashing with anything else on the page
207 // and simplify selectors
208 var CSS = {
209   alpha: 'alpha-d2270',
210   board: 'board-b72b1',
211   chessboard: 'chessboard-63f37',
212   clearfix: 'clearfix-7da63',
213   highlight1: 'highlight1-32417',
214   highlight2: 'highlight2-9c5d2',
215   notation: 'notation-322f9',
216   numeric: 'numeric-fc462',
217   piece: 'piece-417db',
218   row: 'row-5277c',
219   square: 'square-55d63'
220 };
221 var CSSColor = {};
222 CSSColor['white'] = 'white-1e1d7';
223 CSSColor['black'] = 'black-3c85d';
224
225 //------------------------------------------------------------------------------
226 // Module Scope Variables
227 //------------------------------------------------------------------------------
228
229 // DOM elements
230 var containerEl,
231   boardEl,
232   draggedPieceEl;
233
234 // constructor return object
235 var widget = {};
236
237 //------------------------------------------------------------------------------
238 // Stateful
239 //------------------------------------------------------------------------------
240
241 var BOARD_BORDER_SIZE = 2,
242   CURRENT_ORIENTATION = 'white',
243   CURRENT_POSITION = {},
244   SQUARE_SIZE,
245   DRAGGED_PIECE,
246   DRAGGED_PIECE_LOCATION,
247   DRAGGED_PIECE_SOURCE,
248   DRAGGING_A_PIECE = false,
249   SQUARE_ELS_IDS = {},
250   SQUARE_ELS_OFFSETS;
251
252 //------------------------------------------------------------------------------
253 // JS Util Functions
254 //------------------------------------------------------------------------------
255
256 let id_counter = 0;
257 function createId() {
258   return 'chesspiece-id-' + (id_counter++);
259 }
260
261 function deepCopy(thing) {
262   return JSON.parse(JSON.stringify(thing));
263 }
264
265 function parseSemVer(version) {
266   var tmp = version.split('.');
267   return {
268     major: parseInt(tmp[0], 10),
269     minor: parseInt(tmp[1], 10),
270     patch: parseInt(tmp[2], 10)
271   };
272 }
273
274 // returns true if version is >= minimum
275 function compareSemVer(version, minimum) {
276   version = parseSemVer(version);
277   minimum = parseSemVer(minimum);
278
279   var versionNum = (version.major * 10000 * 10000) +
280     (version.minor * 10000) + version.patch;
281   var minimumNum = (minimum.major * 10000 * 10000) +
282     (minimum.minor * 10000) + minimum.patch;
283
284   return (versionNum >= minimumNum);
285 }
286
287 //------------------------------------------------------------------------------
288 // Validation / Errors
289 //------------------------------------------------------------------------------
290
291 /**
292  * @param {!number} code
293  * @param {!string} msg
294  * @param {Object=} obj
295  */
296 function error(code, msg, obj) {
297   // do nothing if showErrors is not set
298   if (cfg.hasOwnProperty('showErrors') !== true ||
299       cfg.showErrors === false) {
300     return;
301   }
302
303   var errorText = 'ChessBoard Error ' + code + ': ' + msg;
304
305   // print to console
306   if (cfg.showErrors === 'console' &&
307       typeof console === 'object' &&
308       typeof console.log === 'function') {
309     console.log(errorText);
310     if (arguments.length >= 2) {
311       console.log(obj);
312     }
313     return;
314   }
315
316   // alert errors
317   if (cfg.showErrors === 'alert') {
318     if (obj) {
319       errorText += '\n\n' + JSON.stringify(obj);
320     }
321     window.alert(errorText);
322     return;
323   }
324
325   // custom function
326   if (typeof cfg.showErrors === 'function') {
327     cfg.showErrors(code, msg, obj);
328   }
329 }
330
331 // check dependencies
332 function checkDeps() {
333   // if containerId is a string, it must be the ID of a DOM node
334   if (typeof containerElOrId === 'string') {
335     // cannot be empty
336     if (containerElOrId === '') {
337       window.alert('ChessBoard Error 1001: ' +
338         'The first argument to ChessBoard() cannot be an empty string.' +
339         '\n\nExiting...');
340       return false;
341     }
342
343     // make sure the container element exists in the DOM
344     var el = document.getElementById(containerElOrId);
345     if (! el) {
346       window.alert('ChessBoard Error 1002: Element with id "' +
347         containerElOrId + '" does not exist in the DOM.' +
348         '\n\nExiting...');
349       return false;
350     }
351
352     // set the containerEl
353     containerEl = el;
354   }
355
356   // else it must be a DOM node
357   else {
358     containerEl = containerElOrId;
359   }
360
361   return true;
362 }
363
364 function validAnimationSpeed(speed) {
365   if (speed === 'fast' || speed === 'slow') {
366     return true;
367   }
368
369   if ((parseInt(speed, 10) + '') !== (speed + '')) {
370     return false;
371   }
372
373   return (speed >= 0);
374 }
375
376 // validate config / set default options
377 function expandConfig() {
378   if (typeof cfg === 'string' || validPositionObject(cfg) === true) {
379     cfg = {
380       position: cfg
381     };
382   }
383
384   // default for orientation is white
385   if (cfg.orientation !== 'black') {
386     cfg.orientation = 'white';
387   }
388   CURRENT_ORIENTATION = cfg.orientation;
389
390   // default for showNotation is true
391   if (cfg.showNotation !== false) {
392     cfg.showNotation = true;
393   }
394
395   // default for draggable is false
396   if (cfg.draggable !== true) {
397     cfg.draggable = false;
398   }
399
400   // default for dropOffBoard is 'snapback'
401   if (cfg.dropOffBoard !== 'trash') {
402     cfg.dropOffBoard = 'snapback';
403   }
404
405   // default piece theme is wikipedia
406   if (cfg.hasOwnProperty('pieceTheme') !== true ||
407       (typeof cfg.pieceTheme !== 'string' &&
408        typeof cfg.pieceTheme !== 'function')) {
409     cfg.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png';
410   }
411
412   // animation speeds
413   if (cfg.hasOwnProperty('appearSpeed') !== true ||
414       validAnimationSpeed(cfg.appearSpeed) !== true) {
415     cfg.appearSpeed = 200;
416   }
417   if (cfg.hasOwnProperty('moveSpeed') !== true ||
418       validAnimationSpeed(cfg.moveSpeed) !== true) {
419     cfg.moveSpeed = 200;
420   }
421   if (cfg.hasOwnProperty('snapbackSpeed') !== true ||
422       validAnimationSpeed(cfg.snapbackSpeed) !== true) {
423     cfg.snapbackSpeed = 50;
424   }
425   if (cfg.hasOwnProperty('snapSpeed') !== true ||
426       validAnimationSpeed(cfg.snapSpeed) !== true) {
427     cfg.snapSpeed = 25;
428   }
429   if (cfg.hasOwnProperty('trashSpeed') !== true ||
430       validAnimationSpeed(cfg.trashSpeed) !== true) {
431     cfg.trashSpeed = 100;
432   }
433
434   // make sure position is valid
435   if (cfg.hasOwnProperty('position') === true) {
436     if (cfg.position === 'start') {
437       CURRENT_POSITION = deepCopy(START_POSITION);
438     }
439
440     else if (validFen(cfg.position) === true) {
441       CURRENT_POSITION = fenToObj(cfg.position);
442     }
443
444     else if (validPositionObject(cfg.position) === true) {
445       CURRENT_POSITION = deepCopy(cfg.position);
446     }
447
448     else {
449       error(7263, 'Invalid value passed to config.position.', cfg.position);
450     }
451   }
452
453   return true;
454 }
455
456 //------------------------------------------------------------------------------
457 // DOM Misc
458 //------------------------------------------------------------------------------
459
460 // calculates square size based on the width of the container
461 // got a little CSS black magic here, so let me explain:
462 // get the width of the container element (could be anything), reduce by 1 for
463 // fudge factor, and then keep reducing until we find an exact mod 8 for
464 // our square size
465 function calculateSquareSize() {
466   var containerWidth = parseInt(getComputedStyle(containerEl).width, 10);
467
468   // defensive, prevent infinite loop
469   if (! containerWidth || containerWidth <= 0) {
470     return 0;
471   }
472
473   // pad one pixel
474   var boardWidth = containerWidth - 1;
475
476   while (boardWidth % 8 !== 0 && boardWidth > 0) {
477     boardWidth--;
478   }
479
480   return (boardWidth / 8);
481 }
482
483 // create random IDs for elements
484 function createElIds() {
485   // squares on the board
486   for (var i = 0; i < COLUMNS.length; i++) {
487     for (var j = 1; j <= 8; j++) {
488       var square = COLUMNS[i] + j;
489       SQUARE_ELS_IDS[square] = square + '-' + createId();
490     }
491   }
492 }
493
494 //------------------------------------------------------------------------------
495 // Markup Building
496 //------------------------------------------------------------------------------
497
498 function buildBoardContainer() {
499   var html = '<div class="' + CSS.chessboard + '">';
500
501   html += '<div class="' + CSS.board + '"></div>';
502
503   html += '</div>';
504
505   return html;
506 }
507
508 /*
509 var buildSquare = function(color, size, id) {
510   var html = '<div class="' + CSS.square + ' ' + CSSColor[color] + '" ' +
511   'style="width: ' + size + 'px; height: ' + size + 'px" ' +
512   'id="' + id + '">';
513
514   if (cfg.showNotation === true) {
515
516   }
517
518   html += '</div>';
519
520   return html;
521 };
522 */
523
524 function buildBoard(orientation) {
525   if (orientation !== 'black') {
526     orientation = 'white';
527   }
528
529   var html = '';
530
531   // algebraic notation / orientation
532   var alpha = deepCopy(COLUMNS);
533   var row = 8;
534   if (orientation === 'black') {
535     alpha.reverse();
536     row = 1;
537   }
538
539   var squareColor = 'white';
540   for (var i = 0; i < 8; i++) {
541     html += '<div class="' + CSS.row + '">';
542     for (var j = 0; j < 8; j++) {
543       var square = alpha[j] + row;
544
545       html += '<div class="' + CSS.square + ' ' + CSSColor[squareColor] + ' ' +
546         'square-' + square + '" ' +
547         'style="width: ' + SQUARE_SIZE + 'px; height: ' + SQUARE_SIZE + 'px" ' +
548         'id="' + SQUARE_ELS_IDS[square] + '" ' +
549         'data-square="' + square + '">';
550
551       if (cfg.showNotation === true) {
552         // alpha notation
553         if ((orientation === 'white' && row === 1) ||
554             (orientation === 'black' && row === 8)) {
555           html += '<div class="' + CSS.notation + ' ' + CSS.alpha + '">' +
556             alpha[j] + '</div>';
557         }
558
559         // numeric notation
560         if (j === 0) {
561           html += '<div class="' + CSS.notation + ' ' + CSS.numeric + '">' +
562             row + '</div>';
563         }
564       }
565
566       html += '</div>'; // end .square
567
568       squareColor = (squareColor === 'white' ? 'black' : 'white');
569     }
570     html += '<div class="' + CSS.clearfix + '"></div></div>';
571
572     squareColor = (squareColor === 'white' ? 'black' : 'white');
573
574     if (orientation === 'white') {
575       row--;
576     }
577     else {
578       row++;
579     }
580   }
581
582   return html;
583 }
584
585 function buildPieceImgSrc(piece) {
586   if (typeof cfg.pieceTheme === 'function') {
587     return cfg.pieceTheme(piece);
588   }
589
590   if (typeof cfg.pieceTheme === 'string') {
591     return cfg.pieceTheme.replace(/{piece}/g, piece);
592   }
593
594   // NOTE: this should never happen
595   error(8272, 'Unable to build image source for cfg.pieceTheme.');
596   return '';
597 }
598
599 /**
600  * @param {!string} piece
601  * @param {boolean=} hidden
602  * @param {!string=} id
603  */
604 function buildPiece(piece, hidden, id) {
605   let img = document.createElement('img');
606   img.src = buildPieceImgSrc(piece);
607   if (id && typeof id === 'string') {
608     img.setAttribute('id', id);
609   }
610   img.setAttribute('alt', '');
611   img.classList.add(CSS.piece);
612   img.setAttribute('data-piece', piece);
613   img.style.width = SQUARE_SIZE + 'px';
614   img.style.height = SQUARE_SIZE + 'px';
615   if (hidden === true) {
616     img.style.display = 'none';
617   }
618   return img;
619 }
620
621 //------------------------------------------------------------------------------
622 // Animations
623 //------------------------------------------------------------------------------
624
625 function offset(el) {  // From https://youmightnotneedjquery.com/.
626   let box = el.getBoundingClientRect();
627   let docElem = document.documentElement;
628   return {
629     top: box.top + window.pageYOffset - docElem.clientTop,
630     left: box.left + window.pageXOffset - docElem.clientLeft
631   };
632 }
633
634 function animateSquareToSquare(src, dest, piece, completeFn) {
635   // get information about the source and destination squares
636   var srcSquareEl = document.getElementById(SQUARE_ELS_IDS[src]);
637   var srcSquarePosition = offset(srcSquareEl);
638   var destSquareEl = document.getElementById(SQUARE_ELS_IDS[dest]);
639   var destSquarePosition = offset(destSquareEl);
640
641   // create the animated piece and absolutely position it
642   // over the source square
643   var animatedPieceId = createId();
644   document.body.append(buildPiece(piece, true, animatedPieceId));
645   var animatedPieceEl = document.getElementById(animatedPieceId);
646   animatedPieceEl.style.display = null;
647   animatedPieceEl.style.position = 'absolute';
648   animatedPieceEl.style.top = srcSquarePosition.top + 'px';
649   animatedPieceEl.style.left = srcSquarePosition.left + 'px';
650
651   // remove original piece(s) from source square
652   // TODO: multiple pieces should never really happen, but it will if we are moving
653   // while another animation still isn't done
654   srcSquareEl.querySelectorAll('.' + CSS.piece).forEach((piece) => piece.remove());
655
656   // on complete
657   var complete = function() {
658     // add the "real" piece to the destination square
659     destSquareEl.append(buildPiece(piece));
660
661     // remove the animated piece
662     animatedPieceEl.remove();
663
664     // run complete function
665     if (typeof completeFn === 'function') {
666       completeFn();
667     }
668   };
669
670   // animate the piece to the destination square
671   animatedPieceEl.addEventListener('transitionend', complete, {once: true});
672   requestAnimationFrame(() => {
673     animatedPieceEl.style.transitionProperty = 'top, left';
674     animatedPieceEl.style.transitionDuration = cfg.moveSpeed + 'ms';
675     animatedPieceEl.style.top = destSquarePosition.top + 'px';
676     animatedPieceEl.style.left = destSquarePosition.left + 'px';
677   });
678 }
679
680 function fadeIn(pieces, onFinish) {
681   pieces.forEach((piece) => {
682     piece.style.opacity = 0;
683     piece.style.display = null;
684     piece.addEventListener('transitionend', onFinish, {once: true});
685   });
686   requestAnimationFrame(() => {
687     pieces.forEach((piece) => {
688       piece.style.transitionProperty = 'opacity';
689       piece.style.transitionDuration = cfg.appearSpeed + 'ms';
690       piece.style.opacity = 1;
691     });
692   });
693 }
694
695 function fadeOut(pieces, onFinish) {
696   pieces.forEach((piece) => {
697     piece.style.opacity = 1;
698     piece.style.display = null;
699     piece.addEventListener('transitionend', onFinish, {once: true});
700   });
701   requestAnimationFrame(() => {
702     pieces.forEach((piece) => {
703       piece.style.transitionProperty = 'opacity';
704       piece.style.transitionDuration = cfg.trashSpeed + 'ms';
705       piece.style.opacity = 0;
706     });
707   });
708 }
709
710 // execute an array of animations
711 function doAnimations(a, oldPos, newPos) {
712   var numFinished = 0;
713   function onFinish(e) {
714     if (e && e.target) {
715       e.target.transitionProperty = null;
716     }
717
718     numFinished++;
719
720     // exit if all the animations aren't finished
721     if (numFinished !== a.length) return;
722
723     drawPositionInstant();
724
725     // run their onMoveEnd function
726     if (cfg.hasOwnProperty('onMoveEnd') === true &&
727       typeof cfg.onMoveEnd === 'function') {
728       cfg.onMoveEnd(deepCopy(oldPos), deepCopy(newPos));
729     }
730   }
731
732   requestAnimationFrame(() => {  // Firefox workaround.
733     let fadeout_pieces = [];
734     let fadein_pieces = [];
735
736     for (var i = 0; i < a.length; i++) {
737       // clear a piece
738       if (a[i].type === 'clear') {
739         document.getElementById(SQUARE_ELS_IDS[a[i].square]).querySelectorAll('.' + CSS.piece).forEach(
740           (piece) => fadeout_pieces.push(piece)
741         );
742       }
743
744       // add a piece
745       if (a[i].type === 'add') {
746         let square = document.getElementById(SQUARE_ELS_IDS[a[i].square]);
747         square.append(buildPiece(a[i].piece, true));
748         let piece = square.querySelector('.' + CSS.piece);
749         fadein_pieces.push(piece);
750       }
751
752       // move a piece
753       if (a[i].type === 'move') {
754         animateSquareToSquare(a[i].source, a[i].destination, a[i].piece,
755           onFinish);
756       }
757     }
758
759     // TODO: Batch moves as well, not just fade in/out.
760     // (We batch them because requestAnimationFrame seemingly costs real time.)
761     if (fadeout_pieces.length > 0) {
762       fadeOut(fadeout_pieces, onFinish);
763     }
764     if (fadein_pieces.length > 0) {
765       fadeIn(fadein_pieces, onFinish);
766     }
767   });
768 }
769
770 // returns the distance between two squares
771 function squareDistance(s1, s2) {
772   s1 = s1.split('');
773   var s1x = COLUMNS.indexOf(s1[0]) + 1;
774   var s1y = parseInt(s1[1], 10);
775
776   s2 = s2.split('');
777   var s2x = COLUMNS.indexOf(s2[0]) + 1;
778   var s2y = parseInt(s2[1], 10);
779
780   var xDelta = Math.abs(s1x - s2x);
781   var yDelta = Math.abs(s1y - s2y);
782
783   if (xDelta >= yDelta) return xDelta;
784   return yDelta;
785 }
786
787 // returns the square of the closest instance of piece
788 // returns false if no instance of piece is found in position
789 function findClosestPiece(position, piece, square) {
790   let best_square = false;
791   let best_dist = 1e9;
792   for (var i = 0; i < COLUMNS.length; i++) {
793     for (var j = 1; j <= 8; j++) {
794       let other_square = COLUMNS[i] + j;
795
796       if (position[other_square] === piece && square != other_square) {
797         let dist = squareDistance(square, other_square);
798         if (dist < best_dist) {
799           best_square = other_square;
800           best_dist = dist;
801         }
802       }
803     }
804   }
805
806   return best_square;
807 }
808
809 // calculate an array of animations that need to happen in order to get
810 // from pos1 to pos2
811 function calculateAnimations(pos1, pos2) {
812   // make copies of both
813   pos1 = deepCopy(pos1);
814   pos2 = deepCopy(pos2);
815
816   var animations = [];
817   var squaresMovedTo = {};
818
819   // remove pieces that are the same in both positions
820   for (var i in pos2) {
821     if (pos2.hasOwnProperty(i) !== true) continue;
822
823     if (pos1.hasOwnProperty(i) === true && pos1[i] === pos2[i]) {
824       delete pos1[i];
825       delete pos2[i];
826     }
827   }
828
829   // find all the "move" animations
830   for (var i in pos2) {
831     if (pos2.hasOwnProperty(i) !== true) continue;
832
833     var closestPiece = findClosestPiece(pos1, pos2[i], i);
834     if (closestPiece !== false) {
835       animations.push({
836         type: 'move',
837         source: closestPiece,
838         destination: i,
839         piece: pos2[i]
840       });
841
842       delete pos1[closestPiece];
843       delete pos2[i];
844       squaresMovedTo[i] = true;
845     }
846   }
847
848   // add pieces to pos2
849   for (var i in pos2) {
850     if (pos2.hasOwnProperty(i) !== true) continue;
851
852     animations.push({
853       type: 'add',
854       square: i,
855       piece: pos2[i]
856     })
857
858     delete pos2[i];
859   }
860
861   // clear pieces from pos1
862   for (var i in pos1) {
863     if (pos1.hasOwnProperty(i) !== true) continue;
864
865     // do not clear a piece if it is on a square that is the result
866     // of a "move", ie: a piece capture
867     if (squaresMovedTo.hasOwnProperty(i) === true) continue;
868
869     animations.push({
870       type: 'clear',
871       square: i,
872       piece: pos1[i]
873     });
874
875     delete pos1[i];
876   }
877
878   return animations;
879 }
880
881 //------------------------------------------------------------------------------
882 // Control Flow
883 //------------------------------------------------------------------------------
884
885 function drawPositionInstant() {
886   // clear the board
887   boardEl.querySelectorAll('.' + CSS.piece).forEach((piece) => piece.remove());
888
889   // add the pieces
890   for (var i in CURRENT_POSITION) {
891     if (CURRENT_POSITION.hasOwnProperty(i) !== true) continue;
892
893     document.getElementById(SQUARE_ELS_IDS[i]).append(buildPiece(CURRENT_POSITION[i]));
894   }
895 }
896
897 function drawBoard() {
898   boardEl.innerHTML = buildBoard(CURRENT_ORIENTATION);
899   drawPositionInstant();
900 }
901
902 // given a position and a set of moves, return a new position
903 // with the moves executed
904 function calculatePositionFromMoves(position, moves) {
905   position = deepCopy(position);
906
907   for (var i in moves) {
908     if (moves.hasOwnProperty(i) !== true) continue;
909
910     // skip the move if the position doesn't have a piece on the source square
911     if (position.hasOwnProperty(i) !== true) continue;
912
913     var piece = position[i];
914     delete position[i];
915     position[moves[i]] = piece;
916   }
917
918   return position;
919 }
920
921 function setCurrentPosition(position) {
922   var oldPos = deepCopy(CURRENT_POSITION);
923   var newPos = deepCopy(position);
924   var oldFen = objToFen(oldPos);
925   var newFen = objToFen(newPos);
926
927   // do nothing if no change in position
928   if (oldFen === newFen) return;
929
930   // run their onChange function
931   if (cfg.hasOwnProperty('onChange') === true &&
932     typeof cfg.onChange === 'function') {
933     cfg.onChange(oldPos, newPos);
934   }
935
936   // update state
937   CURRENT_POSITION = position;
938 }
939
940 function isXYOnSquare(x, y) {
941   for (var i in SQUARE_ELS_OFFSETS) {
942     if (SQUARE_ELS_OFFSETS.hasOwnProperty(i) !== true) continue;
943
944     var s = SQUARE_ELS_OFFSETS[i];
945     if (x >= s.left && x < s.left + SQUARE_SIZE &&
946         y >= s.top && y < s.top + SQUARE_SIZE) {
947       return i;
948     }
949   }
950
951   return 'offboard';
952 }
953
954 // records the XY coords of every square into memory
955 function captureSquareOffsets() {
956   SQUARE_ELS_OFFSETS = {};
957
958   for (var i in SQUARE_ELS_IDS) {
959     if (SQUARE_ELS_IDS.hasOwnProperty(i) !== true) continue;
960
961     SQUARE_ELS_OFFSETS[i] = offset(document.getElementById(SQUARE_ELS_IDS[i]));
962   }
963 }
964
965 function removeSquareHighlights() {
966   boardEl.querySelectorAll('.' + CSS.square).forEach((piece) => {
967     piece.classList.remove(CSS.highlight1);
968     piece.classList.remove(CSS.highlight2);
969   });
970 }
971
972 function snapbackDraggedPiece() {
973   removeSquareHighlights();
974
975   // animation complete
976   function complete() {
977     drawPositionInstant();
978     draggedPieceEl.style.display = 'none';
979
980     // run their onSnapbackEnd function
981     if (cfg.hasOwnProperty('onSnapbackEnd') === true &&
982       typeof cfg.onSnapbackEnd === 'function') {
983       cfg.onSnapbackEnd(DRAGGED_PIECE, DRAGGED_PIECE_SOURCE,
984         deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION);
985     }
986   }
987
988   // get source square position
989   var sourceSquarePosition =
990     offset(document.getElementById(SQUARE_ELS_IDS[DRAGGED_PIECE_SOURCE]));
991
992   // animate the piece to the target square
993   draggedPieceEl.addEventListener('transitionend', complete, {once: true});
994   requestAnimationFrame(() => {
995     draggedPieceEl.style.transitionProperty = 'top, left';
996     draggedPieceEl.style.transitionDuration = cfg.snapbackSpeed + 'ms';
997     draggedPieceEl.style.top = sourceSquarePosition.top + 'px';
998     draggedPieceEl.style.left = sourceSquarePosition.left + 'px';
999   });
1000
1001   // set state
1002   DRAGGING_A_PIECE = false;
1003 }
1004
1005 function trashDraggedPiece() {
1006   removeSquareHighlights();
1007
1008   // remove the source piece
1009   var newPosition = deepCopy(CURRENT_POSITION);
1010   delete newPosition[DRAGGED_PIECE_SOURCE];
1011   setCurrentPosition(newPosition);
1012
1013   // redraw the position
1014   drawPositionInstant();
1015
1016   // hide the dragged piece
1017   // FIXME: support this for non-jquery
1018   //$(draggedPieceEl).fadeOut(cfg.trashSpeed);
1019
1020   // set state
1021   DRAGGING_A_PIECE = false;
1022 }
1023
1024 function dropDraggedPieceOnSquare(square) {
1025   removeSquareHighlights();
1026
1027   // update position
1028   var newPosition = deepCopy(CURRENT_POSITION);
1029   delete newPosition[DRAGGED_PIECE_SOURCE];
1030   newPosition[square] = DRAGGED_PIECE;
1031   setCurrentPosition(newPosition);
1032
1033   // get target square information
1034   var targetSquarePosition = offset(document.getElementById(SQUARE_ELS_IDS[square]));
1035
1036   // animation complete
1037   var complete = function() {
1038     drawPositionInstant();
1039     draggedPieceEl.style.display = 'none';
1040
1041     // execute their onSnapEnd function
1042     if (cfg.hasOwnProperty('onSnapEnd') === true &&
1043       typeof cfg.onSnapEnd === 'function') {
1044       requestAnimationFrame(() => {  // HACK: so that we don't add event handlers from the callback...
1045         cfg.onSnapEnd(DRAGGED_PIECE_SOURCE, square, DRAGGED_PIECE);
1046       });
1047     }
1048   };
1049
1050   // snap the piece to the target square
1051   draggedPieceEl.addEventListener('transitionend', complete, {once: true});
1052   requestAnimationFrame(() => {
1053     draggedPieceEl.style.transitionProperty = 'top, left';
1054     draggedPieceEl.style.transitionDuration = cfg.snapSpeed + 'ms';
1055     draggedPieceEl.style.top = targetSquarePosition.top + 'px';
1056     draggedPieceEl.style.left = targetSquarePosition.left + 'px';
1057   });
1058
1059   // set state
1060   DRAGGING_A_PIECE = false;
1061 }
1062
1063 function beginDraggingPiece(source, piece, x, y) {
1064   // run their custom onDragStart function
1065   // their custom onDragStart function can cancel drag start
1066   if (typeof cfg.onDragStart === 'function' &&
1067       cfg.onDragStart(source, piece,
1068         deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION) === false) {
1069     return;
1070   }
1071
1072   // set state
1073   DRAGGING_A_PIECE = true;
1074   DRAGGED_PIECE = piece;
1075   DRAGGED_PIECE_SOURCE = source;
1076   DRAGGED_PIECE_LOCATION = source;
1077
1078   // capture the x, y coords of all squares in memory
1079   captureSquareOffsets();
1080
1081   // create the dragged piece
1082   draggedPieceEl.setAttribute('src', buildPieceImgSrc(piece));
1083   draggedPieceEl.style.display = null;
1084   draggedPieceEl.style.position = 'absolute';
1085   draggedPieceEl.style.left = (x - (SQUARE_SIZE / 2)) + 'px';
1086   draggedPieceEl.style.top = (y - (SQUARE_SIZE / 2)) + 'px';
1087
1088   // highlight the source square and hide the piece
1089   let square = document.getElementById(SQUARE_ELS_IDS[source]);
1090   square.classList.add(CSS.highlight1);
1091   square.querySelector('.' + CSS.piece).style.display = 'none';
1092 }
1093
1094 function updateDraggedPiece(x, y) {
1095   // put the dragged piece over the mouse cursor
1096   draggedPieceEl.style.left = (x - (SQUARE_SIZE / 2)) + 'px';
1097   draggedPieceEl.style.top = (y - (SQUARE_SIZE / 2)) + 'px';
1098
1099   // get location
1100   var location = isXYOnSquare(x, y);
1101
1102   // do nothing if the location has not changed
1103   if (location === DRAGGED_PIECE_LOCATION) return;
1104
1105   // remove highlight from previous square
1106   if (validSquare(DRAGGED_PIECE_LOCATION) === true) {
1107     document.getElementById(SQUARE_ELS_IDS[DRAGGED_PIECE_LOCATION])
1108       .classList.remove(CSS.highlight2);
1109   }
1110
1111   // add highlight to new square
1112   if (validSquare(location) === true) {
1113     document.getElementById(SQUARE_ELS_IDS[location]).classList.add(CSS.highlight2);
1114   }
1115
1116   // run onDragMove
1117   if (typeof cfg.onDragMove === 'function') {
1118     cfg.onDragMove(location, DRAGGED_PIECE_LOCATION,
1119       DRAGGED_PIECE_SOURCE, DRAGGED_PIECE,
1120       deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION);
1121   }
1122
1123   // update state
1124   DRAGGED_PIECE_LOCATION = location;
1125 }
1126
1127 function stopDraggedPiece(location) {
1128   // determine what the action should be
1129   var action = 'drop';
1130   if (location === 'offboard' && cfg.dropOffBoard === 'snapback') {
1131     action = 'snapback';
1132   }
1133   if (location === 'offboard' && cfg.dropOffBoard === 'trash') {
1134     action = 'trash';
1135   }
1136
1137   // run their onDrop function, which can potentially change the drop action
1138   if (cfg.hasOwnProperty('onDrop') === true &&
1139     typeof cfg.onDrop === 'function') {
1140     var newPosition = deepCopy(CURRENT_POSITION);
1141
1142     // source piece was on the board and position is off the board
1143     if (validSquare(DRAGGED_PIECE_SOURCE) === true && location === 'offboard') {
1144       // remove the piece from the board
1145       delete newPosition[DRAGGED_PIECE_SOURCE];
1146     }
1147
1148     // source piece was on the board and position is on the board
1149     if (validSquare(DRAGGED_PIECE_SOURCE) === true &&
1150       validSquare(location) === true) {
1151       // move the piece
1152       delete newPosition[DRAGGED_PIECE_SOURCE];
1153       newPosition[location] = DRAGGED_PIECE;
1154     }
1155
1156     var oldPosition = deepCopy(CURRENT_POSITION);
1157
1158     var result = cfg.onDrop(DRAGGED_PIECE_SOURCE, location, DRAGGED_PIECE,
1159       newPosition, oldPosition, CURRENT_ORIENTATION);
1160     if (result === 'snapback' || result === 'trash') {
1161       action = result;
1162     }
1163   }
1164
1165   // do it!
1166   if (action === 'snapback') {
1167     snapbackDraggedPiece();
1168   }
1169   else if (action === 'trash') {
1170     trashDraggedPiece();
1171   }
1172   else if (action === 'drop') {
1173     dropDraggedPieceOnSquare(location);
1174   }
1175 }
1176
1177 //------------------------------------------------------------------------------
1178 // Public Methods
1179 //------------------------------------------------------------------------------
1180
1181 // clear the board
1182 widget.clear = function(useAnimation) {
1183   widget.position({}, useAnimation);
1184 };
1185
1186 /*
1187 // get or set config properties
1188 // TODO: write this, GitHub Issue #1
1189 widget.config = function(arg1, arg2) {
1190   // get the current config
1191   if (arguments.length === 0) {
1192     return deepCopy(cfg);
1193   }
1194 };
1195 */
1196
1197 // remove the widget from the page
1198 widget.destroy = function() {
1199   // remove markup
1200   containerEl.innerHTML = '';
1201   draggedPieceEl.remove();
1202 };
1203
1204 // shorthand method to get the current FEN
1205 widget.fen = function() {
1206   return widget.position('fen');
1207 };
1208
1209 // flip orientation
1210 widget.flip = function() {
1211   widget.orientation('flip');
1212 };
1213
1214 /*
1215 // TODO: write this, GitHub Issue #5
1216 widget.highlight = function() {
1217
1218 };
1219 */
1220
1221 // move pieces
1222 widget.move = function() {
1223   // no need to throw an error here; just do nothing
1224   if (arguments.length === 0) return;
1225
1226   var useAnimation = true;
1227
1228   // collect the moves into an object
1229   var moves = {};
1230   for (var i = 0; i < arguments.length; i++) {
1231     // any "false" to this function means no animations
1232     if (arguments[i] === false) {
1233       useAnimation = false;
1234       continue;
1235     }
1236
1237     // skip invalid arguments
1238     if (validMove(arguments[i]) !== true) {
1239       error(2826, 'Invalid move passed to the move method.', arguments[i]);
1240       continue;
1241     }
1242
1243     var tmp = arguments[i].split('-');
1244     moves[tmp[0]] = tmp[1];
1245   }
1246
1247   // calculate position from moves
1248   var newPos = calculatePositionFromMoves(CURRENT_POSITION, moves);
1249
1250   // update the board
1251   widget.position(newPos, useAnimation);
1252
1253   // return the new position object
1254   return newPos;
1255 };
1256
1257 widget.orientation = function(arg) {
1258   // no arguments, return the current orientation
1259   if (arguments.length === 0) {
1260     return CURRENT_ORIENTATION;
1261   }
1262
1263   // set to white or black
1264   if (arg === 'white' || arg === 'black') {
1265     CURRENT_ORIENTATION = arg;
1266     drawBoard();
1267     return;
1268   }
1269
1270   // flip orientation
1271   if (arg === 'flip') {
1272     CURRENT_ORIENTATION = (CURRENT_ORIENTATION === 'white') ? 'black' : 'white';
1273     drawBoard();
1274     return;
1275   }
1276
1277   error(5482, 'Invalid value passed to the orientation method.', arg);
1278 };
1279
1280 /**
1281  * @param {!string|!Object} position
1282  * @param {boolean=} useAnimation
1283  */
1284 widget.position = function(position, useAnimation) {
1285   // no arguments, return the current position
1286   if (arguments.length === 0) {
1287     return deepCopy(CURRENT_POSITION);
1288   }
1289
1290   // get position as FEN
1291   if (typeof position === 'string' && position.toLowerCase() === 'fen') {
1292     return objToFen(CURRENT_POSITION);
1293   }
1294
1295   // default for useAnimations is true
1296   if (useAnimation !== false) {
1297     useAnimation = true;
1298   }
1299
1300   // start position
1301   if (typeof position === 'string' && position.toLowerCase() === 'start') {
1302     position = deepCopy(START_POSITION);
1303   }
1304
1305   // convert FEN to position object
1306   if (validFen(position) === true) {
1307     position = fenToObj(position);
1308   }
1309
1310   // validate position object
1311   if (validPositionObject(position) !== true) {
1312     error(6482, 'Invalid value passed to the position method.', position);
1313     return;
1314   }
1315
1316   if (useAnimation === true) {
1317     // start the animations
1318     doAnimations(calculateAnimations(CURRENT_POSITION, position),
1319       CURRENT_POSITION, position);
1320
1321     // set the new position
1322     setCurrentPosition(position);
1323   }
1324   // instant update
1325   else {
1326     setCurrentPosition(position);
1327     drawPositionInstant();
1328   }
1329 };
1330
1331 widget.resize = function() {
1332   // calulate the new square size
1333   SQUARE_SIZE = calculateSquareSize();
1334
1335   // set board width
1336   boardEl.style.width = (SQUARE_SIZE * 8) + 'px';
1337
1338   // set drag piece size
1339   if (draggedPieceEl !== null) {
1340     draggedPieceEl.style.height = SQUARE_SIZE + 'px';
1341     draggedPieceEl.style.width = SQUARE_SIZE + 'px';
1342   }
1343
1344   // redraw the board
1345   drawBoard();
1346 };
1347
1348 // set the starting position
1349 widget.start = function(useAnimation) {
1350   widget.position('start', useAnimation);
1351 };
1352
1353 //------------------------------------------------------------------------------
1354 // Browser Events
1355 //------------------------------------------------------------------------------
1356
1357 function isTouchDevice() {
1358   return ('ontouchstart' in document.documentElement);
1359 }
1360
1361 function mousedownSquare(e) {
1362   let target = e.target.closest('.' + CSS.square);
1363   if (!target) {
1364     return;
1365   }
1366
1367   // do nothing if we're not draggable
1368   if (cfg.draggable !== true) return;
1369
1370   var square = target.getAttribute('data-square');
1371
1372   // no piece on this square
1373   if (validSquare(square) !== true ||
1374       CURRENT_POSITION.hasOwnProperty(square) !== true) {
1375     return;
1376   }
1377
1378   beginDraggingPiece(square, CURRENT_POSITION[square], e.pageX, e.pageY);
1379 }
1380
1381 function touchstartSquare(e) {
1382   let target = e.target.closest('.' + CSS.square);
1383   if (!target) {
1384     return;
1385   }
1386
1387   // do nothing if we're not draggable
1388   if (cfg.draggable !== true) return;
1389
1390   var square = target.getAttribute('data-square');
1391
1392   // no piece on this square
1393   if (validSquare(square) !== true ||
1394       CURRENT_POSITION.hasOwnProperty(square) !== true) {
1395     return;
1396   }
1397
1398   beginDraggingPiece(square, CURRENT_POSITION[square],
1399     e.changedTouches[0].pageX, e.changedTouches[0].pageY);
1400 }
1401
1402 function mousemoveWindow(e) {
1403   // do nothing if we are not dragging a piece
1404   if (DRAGGING_A_PIECE !== true) return;
1405
1406   updateDraggedPiece(e.pageX, e.pageY);
1407 }
1408
1409 function touchmoveWindow(e) {
1410   // do nothing if we are not dragging a piece
1411   if (DRAGGING_A_PIECE !== true) return;
1412
1413   // prevent screen from scrolling
1414   e.preventDefault();
1415
1416   updateDraggedPiece(e.changedTouches[0].pageX,
1417     e.changedTouches[0].pageY);
1418 }
1419
1420 function mouseupWindow(e) {
1421   // do nothing if we are not dragging a piece
1422   if (DRAGGING_A_PIECE !== true) return;
1423
1424   // get the location
1425   var location = isXYOnSquare(e.pageX, e.pageY);
1426
1427   stopDraggedPiece(location);
1428 }
1429
1430 function touchendWindow(e) {
1431   // do nothing if we are not dragging a piece
1432   if (DRAGGING_A_PIECE !== true) return;
1433
1434   // get the location
1435   var location = isXYOnSquare(e.changedTouches[0].pageX,
1436     e.changedTouches[0].pageY);
1437
1438   stopDraggedPiece(location);
1439 }
1440
1441 function mouseenterSquare(e) {
1442   let target = e.target.closest('.' + CSS.square);
1443   if (!target) {
1444     return;
1445   }
1446
1447   // do not fire this event if we are dragging a piece
1448   // NOTE: this should never happen, but it's a safeguard
1449   if (DRAGGING_A_PIECE !== false) return;
1450
1451   if (cfg.hasOwnProperty('onMouseoverSquare') !== true ||
1452     typeof cfg.onMouseoverSquare !== 'function') return;
1453
1454   // get the square
1455   var square = target.getAttribute('data-square');
1456
1457   // NOTE: this should never happen; defensive
1458   if (validSquare(square) !== true) return;
1459
1460   // get the piece on this square
1461   var piece = false;
1462   if (CURRENT_POSITION.hasOwnProperty(square) === true) {
1463     piece = CURRENT_POSITION[square];
1464   }
1465
1466   // execute their function
1467   cfg.onMouseoverSquare(square, piece, deepCopy(CURRENT_POSITION),
1468     CURRENT_ORIENTATION);
1469 }
1470
1471 function mouseleaveSquare(e) {
1472   let target = e.target.closest('.' + CSS.square);
1473   if (!target) {
1474     return;
1475   }
1476
1477   // do not fire this event if we are dragging a piece
1478   // NOTE: this should never happen, but it's a safeguard
1479   if (DRAGGING_A_PIECE !== false) return;
1480
1481   if (cfg.hasOwnProperty('onMouseoutSquare') !== true ||
1482     typeof cfg.onMouseoutSquare !== 'function') return;
1483
1484   // get the square
1485   var square = target.getAttribute('data-square');
1486
1487   // NOTE: this should never happen; defensive
1488   if (validSquare(square) !== true) return;
1489
1490   // get the piece on this square
1491   var piece = false;
1492   if (CURRENT_POSITION.hasOwnProperty(square) === true) {
1493     piece = CURRENT_POSITION[square];
1494   }
1495
1496   // execute their function
1497   cfg.onMouseoutSquare(square, piece, deepCopy(CURRENT_POSITION),
1498     CURRENT_ORIENTATION);
1499 }
1500
1501 //------------------------------------------------------------------------------
1502 // Initialization
1503 //------------------------------------------------------------------------------
1504
1505 function addEvents() {
1506   // prevent browser "image drag"
1507   let stopDefault = (e) => {
1508     if (e.target.matches('.' + CSS.piece)) {
1509       e.preventDefault();
1510     }
1511   };
1512   document.body.addEventListener('mousedown', stopDefault);
1513   document.body.addEventListener('mousemove', stopDefault);
1514
1515   // mouse drag pieces
1516   boardEl.addEventListener('mousedown', mousedownSquare);
1517
1518   // mouse enter / leave square
1519   boardEl.addEventListener('mouseenter', mouseenterSquare);
1520   boardEl.addEventListener('mouseleave', mouseleaveSquare);
1521
1522   window.addEventListener('mousemove', mousemoveWindow);
1523   window.addEventListener('mouseup', mouseupWindow);
1524
1525   // touch drag pieces
1526   if (isTouchDevice() === true) {
1527     boardEl.addEventListener('touchstart', touchstartSquare);
1528     window.addEventListener('touchmove', touchmoveWindow);
1529     window.addEventListener('touchend', touchendWindow);
1530   }
1531 }
1532
1533 function initDom() {
1534   // build board and save it in memory
1535   containerEl.innerHTML = buildBoardContainer();
1536   boardEl = containerEl.querySelector('.' + CSS.board);
1537
1538   // create the drag piece
1539   var draggedPieceId = createId();
1540   document.body.append(buildPiece('wP', true, draggedPieceId));
1541   draggedPieceEl = document.getElementById(draggedPieceId);
1542
1543   // get the border size
1544   BOARD_BORDER_SIZE = parseInt(boardEl.style.borderLeftWidth, 10);
1545
1546   // set the size and draw the board
1547   widget.resize();
1548 }
1549
1550 function init() {
1551   if (checkDeps() !== true ||
1552       expandConfig() !== true) return;
1553
1554   // create unique IDs for all the elements we will create
1555   createElIds();
1556
1557   initDom();
1558   addEvents();
1559 }
1560
1561 // go time
1562 init();
1563
1564 // return the widget object
1565 return widget;
1566
1567 }; // end window.ChessBoard
1568
1569 // expose util functions
1570 window.ChessBoard.fenToObj = fenToObj;
1571 window.ChessBoard.objToFen = objToFen;
1572
1573 })(); // end anonymous wrapper