chessboard.js 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183
  1. /*!
  2. * chessboard.js v0.3.0
  3. *
  4. * Copyright 2013 Chris Oakman
  5. * Released under the MIT license
  6. * http://chessboardjs.com/license
  7. *
  8. * Date: 10 Aug 2013
  9. */
  10. // start anonymous scope
  11. ;(function() {
  12. 'use strict';
  13. //------------------------------------------------------------------------------
  14. // Chess Util Functions
  15. //------------------------------------------------------------------------------
  16. var COLUMNS = 'abcdefg'.split('');
  17. function validMove(move) {
  18. // move should be a string
  19. if (typeof move !== 'string') return false;
  20. // move should be in the form of "e2-e4", "f6-d5"
  21. var tmp = move.split('-');
  22. if (tmp.length !== 2) return false;
  23. return (validSquare(tmp[0]) === true && validSquare(tmp[1]) === true);
  24. }
  25. function validSquare(square) {
  26. if (typeof square !== 'string') return false;
  27. return (square.search(/^[a-h][1-7]$/) !== -1);
  28. }
  29. function validPieceCode(code) {
  30. if (typeof code !== 'string') return false;
  31. return (code.search(/^[bw][N]$/) !== -1);
  32. }
  33. // TODO: this whole function could probably be replaced with a single regex
  34. function validFen(fen) {
  35. if (typeof fen !== 'string') return false;
  36. // cut off any move, castling, etc info from the end
  37. // we're only interested in position information
  38. fen = fen.replace(/ .+$/, '');
  39. // FEN should be 8 sections separated by slashes
  40. var chunks = fen.split('/');
  41. if (chunks.length !== 7) return false;
  42. // check the piece sections
  43. for (var i = 0; i < 7; i++) {
  44. if (chunks[i] === '' ||
  45. chunks[i].length > 7 ||
  46. chunks[i].search(/[^nN1-7]/) !== -1) {
  47. return false;
  48. }
  49. }
  50. return true;
  51. }
  52. function validPositionObject(pos) {
  53. if (typeof pos !== 'object') return false;
  54. for (var i in pos) {
  55. if (pos.hasOwnProperty(i) !== true) continue;
  56. if (validSquare(i) !== true || validPieceCode(pos[i]) !== true) {
  57. return false;
  58. }
  59. }
  60. return true;
  61. }
  62. // convert FEN piece code to bP, wK, etc
  63. function fenToPieceCode(piece) {
  64. // black piece
  65. if (piece.toLowerCase() === piece) {
  66. return 'b' + piece.toUpperCase();
  67. }
  68. // white piece
  69. return 'w' + piece.toUpperCase();
  70. }
  71. // convert bP, wK, etc code to FEN structure
  72. function pieceCodeToFen(piece) {
  73. var tmp = piece.split('');
  74. // white piece
  75. if (tmp[0] === 'w') {
  76. return tmp[1].toUpperCase();
  77. }
  78. // black piece
  79. return tmp[1].toLowerCase();
  80. }
  81. // convert FEN string to position object
  82. // returns false if the FEN string is invalid
  83. function fenToObj(fen) {
  84. if (validFen(fen) !== true) {
  85. return false;
  86. }
  87. // cut off any move, castling, etc info from the end
  88. // we're only interested in position information
  89. fen = fen.replace(/ .+$/, '');
  90. var rows = fen.split('/');
  91. var position = {};
  92. var currentRow = 7;
  93. for (var i = 0; i < 7; i++) {
  94. var row = rows[i].split('');
  95. var colIndex = 0;
  96. // loop through each character in the FEN section
  97. for (var j = 0; j < row.length; j++) {
  98. // number / empty squares
  99. if (row[j].search(/[1-7]/) !== -1) {
  100. var emptySquares = parseInt(row[j], 10);
  101. colIndex += emptySquares;
  102. }
  103. // piece
  104. else {
  105. var square = COLUMNS[colIndex] + currentRow;
  106. position[square] = fenToPieceCode(row[j]);
  107. colIndex++;
  108. }
  109. }
  110. currentRow--;
  111. }
  112. return position;
  113. }
  114. // position object to FEN string
  115. // returns false if the obj is not a valid position object
  116. function objToFen(obj) {
  117. if (validPositionObject(obj) !== true) {
  118. return false;
  119. }
  120. var fen = '';
  121. var currentRow = 7;
  122. for (var i = 0; i < 7; i++) {
  123. for (var j = 0; j < 7; j++) {
  124. var square = COLUMNS[j] + currentRow;
  125. // piece exists
  126. if (obj.hasOwnProperty(square) === true) {
  127. fen += pieceCodeToFen(obj[square]);
  128. }
  129. // empty space
  130. else {
  131. fen += '1';
  132. }
  133. }
  134. if (i !== 6) {
  135. fen += '/';
  136. }
  137. currentRow--;
  138. }
  139. // squeeze the numbers together
  140. // haha, I love this solution...
  141. fen = fen.replace(/1111111/g, '7');
  142. fen = fen.replace(/111111/g, '6');
  143. fen = fen.replace(/11111/g, '5');
  144. fen = fen.replace(/1111/g, '4');
  145. fen = fen.replace(/111/g, '3');
  146. fen = fen.replace(/11/g, '2');
  147. return fen;
  148. }
  149. window['ChessBoard'] = window['ChessBoard'] || function(containerElOrId, cfg) {
  150. 'use strict';
  151. cfg = cfg || {};
  152. //------------------------------------------------------------------------------
  153. // Constants
  154. //------------------------------------------------------------------------------
  155. var MINIMUM_JQUERY_VERSION = '1.7.0',
  156. START_FEN = '7/7/7/7/7/7/7/7',
  157. START_POSITION = fenToObj(START_FEN);
  158. // use unique class names to prevent clashing with anything else on the page
  159. // and simplify selectors
  160. var CSS = {
  161. alpha: 'alpha-d2270',
  162. black: 'black-3c85d',
  163. board: 'board-b72b1',
  164. chessboard: 'chessboard-63f37',
  165. clearfix: 'clearfix-7da63',
  166. highlight1: 'highlight1-32417',
  167. highlight2: 'highlight2-9c5d2',
  168. notation: 'notation-322f9',
  169. numeric: 'numeric-fc462',
  170. piece: 'piece-417db',
  171. row: 'row-5277c',
  172. sparePieces: 'spare-pieces-7492f',
  173. sparePiecesBottom: 'spare-pieces-bottom-ae20f',
  174. sparePiecesTop: 'spare-pieces-top-4028b',
  175. square: 'square-55d63',
  176. white: 'white-1e1d7'
  177. };
  178. //------------------------------------------------------------------------------
  179. // Module Scope Variables
  180. //------------------------------------------------------------------------------
  181. // DOM elements
  182. var containerEl,
  183. boardEl,
  184. draggedPieceEl,
  185. sparePiecesTopEl,
  186. sparePiecesBottomEl;
  187. // constructor return object
  188. var widget = {};
  189. //------------------------------------------------------------------------------
  190. // Stateful
  191. //------------------------------------------------------------------------------
  192. var ANIMATION_HAPPENING = false,
  193. BOARD_BORDER_SIZE = 2,
  194. CURRENT_ORIENTATION = 'white',
  195. CURRENT_POSITION = {},
  196. SQUARE_SIZE,
  197. DRAGGED_PIECE,
  198. DRAGGED_PIECE_LOCATION,
  199. DRAGGED_PIECE_SOURCE,
  200. DRAGGING_A_PIECE = false,
  201. SPARE_PIECE_ELS_IDS = {},
  202. SQUARE_ELS_IDS = {},
  203. SQUARE_ELS_OFFSETS;
  204. //------------------------------------------------------------------------------
  205. // JS Util Functions
  206. //------------------------------------------------------------------------------
  207. // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
  208. function createId() {
  209. return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function(c) {
  210. var r = Math.random() * 16 | 0;
  211. return r.toString(16);
  212. });
  213. }
  214. function deepCopy(thing) {
  215. return JSON.parse(JSON.stringify(thing));
  216. }
  217. function parseSemVer(version) {
  218. var tmp = version.split('.');
  219. return {
  220. major: parseInt(tmp[0], 10),
  221. minor: parseInt(tmp[1], 10),
  222. patch: parseInt(tmp[2], 10)
  223. };
  224. }
  225. // returns true if version is >= minimum
  226. function compareSemVer(version, minimum) {
  227. version = parseSemVer(version);
  228. minimum = parseSemVer(minimum);
  229. var versionNum = (version.major * 10000 * 10000) +
  230. (version.minor * 10000) + version.patch;
  231. var minimumNum = (minimum.major * 10000 * 10000) +
  232. (minimum.minor * 10000) + minimum.patch;
  233. return (versionNum >= minimumNum);
  234. }
  235. //------------------------------------------------------------------------------
  236. // Validation / Errors
  237. //------------------------------------------------------------------------------
  238. function error(code, msg, obj) {
  239. // do nothing if showErrors is not set
  240. if (cfg.hasOwnProperty('showErrors') !== true ||
  241. cfg.showErrors === false) {
  242. return;
  243. }
  244. var errorText = 'ChessBoard Error ' + code + ': ' + msg;
  245. // print to console
  246. if (cfg.showErrors === 'console' &&
  247. typeof console === 'object' &&
  248. typeof console.log === 'function') {
  249. console.log(errorText);
  250. if (arguments.length >= 2) {
  251. console.log(obj);
  252. }
  253. return;
  254. }
  255. // alert errors
  256. if (cfg.showErrors === 'alert') {
  257. if (obj) {
  258. errorText += '\n\n' + JSON.stringify(obj);
  259. }
  260. window.alert(errorText);
  261. return;
  262. }
  263. // custom function
  264. if (typeof cfg.showErrors === 'function') {
  265. cfg.showErrors(code, msg, obj);
  266. }
  267. }
  268. // check dependencies
  269. function checkDeps() {
  270. // if containerId is a string, it must be the ID of a DOM node
  271. if (typeof containerElOrId === 'string') {
  272. // cannot be empty
  273. if (containerElOrId === '') {
  274. window.alert('ChessBoard Error 1001: ' +
  275. 'The first argument to ChessBoard() cannot be an empty string.' +
  276. '\n\nExiting...');
  277. return false;
  278. }
  279. // make sure the container element exists in the DOM
  280. var el = document.getElementById(containerElOrId);
  281. if (! el) {
  282. window.alert('ChessBoard Error 1002: Element with id "' +
  283. containerElOrId + '" does not exist in the DOM.' +
  284. '\n\nExiting...');
  285. return false;
  286. }
  287. // set the containerEl
  288. containerEl = $(el);
  289. }
  290. // else it must be something that becomes a jQuery collection
  291. // with size 1
  292. // ie: a single DOM node or jQuery object
  293. else {
  294. containerEl = $(containerElOrId);
  295. if (containerEl.length !== 1) {
  296. window.alert('ChessBoard Error 1003: The first argument to ' +
  297. 'ChessBoard() must be an ID or a single DOM node.' +
  298. '\n\nExiting...');
  299. return false;
  300. }
  301. }
  302. // JSON must exist
  303. if (! window.JSON ||
  304. typeof JSON.stringify !== 'function' ||
  305. typeof JSON.parse !== 'function') {
  306. window.alert('ChessBoard Error 1004: JSON does not exist. ' +
  307. 'Please include a JSON polyfill.\n\nExiting...');
  308. return false;
  309. }
  310. // check for a compatible version of jQuery
  311. if (! (typeof window.$ && $.fn && $.fn.jquery &&
  312. compareSemVer($.fn.jquery, MINIMUM_JQUERY_VERSION) === true)) {
  313. window.alert('ChessBoard Error 1005: Unable to find a valid version ' +
  314. 'of jQuery. Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or ' +
  315. 'higher on the page.\n\nExiting...');
  316. return false;
  317. }
  318. return true;
  319. }
  320. function validAnimationSpeed(speed) {
  321. if (speed === 'fast' || speed === 'slow') {
  322. return true;
  323. }
  324. if ((parseInt(speed, 10) + '') !== (speed + '')) {
  325. return false;
  326. }
  327. return (speed >= 0);
  328. }
  329. // validate config / set default options
  330. function expandConfig() {
  331. if (typeof cfg === 'string' || validPositionObject(cfg) === true) {
  332. cfg = {
  333. position: cfg
  334. };
  335. }
  336. // default for orientation is white
  337. if (cfg.orientation !== 'black') {
  338. cfg.orientation = 'white';
  339. }
  340. CURRENT_ORIENTATION = cfg.orientation;
  341. // default for showNotation is true
  342. if (cfg.showNotation !== false) {
  343. cfg.showNotation = true;
  344. }
  345. // default for draggable is false
  346. if (cfg.draggable !== true) {
  347. cfg.draggable = false;
  348. }
  349. // default for dropOffBoard is 'snapback'
  350. if (cfg.dropOffBoard !== 'trash') {
  351. cfg.dropOffBoard = 'snapback';
  352. }
  353. // default for sparePieces is false
  354. if (cfg.sparePieces !== true) {
  355. cfg.sparePieces = false;
  356. }
  357. // draggable must be true if sparePieces is enabled
  358. if (cfg.sparePieces === true) {
  359. cfg.draggable = true;
  360. }
  361. // default piece theme is wikipedia
  362. if (cfg.hasOwnProperty('pieceTheme') !== true ||
  363. (typeof cfg.pieceTheme !== 'string' &&
  364. typeof cfg.pieceTheme !== 'function')) {
  365. cfg.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png';
  366. }
  367. // animation speeds
  368. if (cfg.hasOwnProperty('appearSpeed') !== true ||
  369. validAnimationSpeed(cfg.appearSpeed) !== true) {
  370. cfg.appearSpeed = 200;
  371. }
  372. if (cfg.hasOwnProperty('moveSpeed') !== true ||
  373. validAnimationSpeed(cfg.moveSpeed) !== true) {
  374. cfg.moveSpeed = 200;
  375. }
  376. if (cfg.hasOwnProperty('snapbackSpeed') !== true ||
  377. validAnimationSpeed(cfg.snapbackSpeed) !== true) {
  378. cfg.snapbackSpeed = 50;
  379. }
  380. if (cfg.hasOwnProperty('snapSpeed') !== true ||
  381. validAnimationSpeed(cfg.snapSpeed) !== true) {
  382. cfg.snapSpeed = 25;
  383. }
  384. if (cfg.hasOwnProperty('trashSpeed') !== true ||
  385. validAnimationSpeed(cfg.trashSpeed) !== true) {
  386. cfg.trashSpeed = 100;
  387. }
  388. // make sure position is valid
  389. if (cfg.hasOwnProperty('position') === true) {
  390. if (cfg.position === 'start') {
  391. CURRENT_POSITION = deepCopy(START_POSITION);
  392. }
  393. else if (validFen(cfg.position) === true) {
  394. CURRENT_POSITION = fenToObj(cfg.position);
  395. }
  396. else if (validPositionObject(cfg.position) === true) {
  397. CURRENT_POSITION = deepCopy(cfg.position);
  398. }
  399. else {
  400. error(7263, 'Invalid value passed to config.position.', cfg.position);
  401. }
  402. }
  403. return true;
  404. }
  405. //------------------------------------------------------------------------------
  406. // DOM Misc
  407. //------------------------------------------------------------------------------
  408. // calculates square size based on the width of the container
  409. // got a little CSS black magic here, so let me explain:
  410. // get the width of the container element (could be anything), reduce by 1 for
  411. // fudge factor, and then keep reducing until we find an exact mod 8 for
  412. // our square size
  413. function calculateSquareSize() {
  414. var containerWidth = parseInt(containerEl.css('width'), 10);
  415. // defensive, prevent infinite loop
  416. if (! containerWidth || containerWidth <= 0) {
  417. return 0;
  418. }
  419. // pad one pixel
  420. var boardWidth = containerWidth - 1;
  421. while (boardWidth % 7 !== 0 && boardWidth > 0) {
  422. boardWidth--;
  423. }
  424. return (boardWidth / 7);
  425. }
  426. // create random IDs for elements
  427. function createElIds() {
  428. // squares on the board
  429. for (var i = 0; i < COLUMNS.length; i++) {
  430. for (var j = 1; j <= 7; j++) {
  431. var square = COLUMNS[i] + j;
  432. SQUARE_ELS_IDS[square] = square + '-' + createId();
  433. }
  434. }
  435. }
  436. //------------------------------------------------------------------------------
  437. // Markup Building
  438. //------------------------------------------------------------------------------
  439. function buildBoardContainer() {
  440. var html = '<div class="' + CSS.chessboard + '">';
  441. html += '<div class="' + CSS.board + '"></div>';
  442. html += '</div>';
  443. return html;
  444. }
  445. /*
  446. var buildSquare = function(color, size, id) {
  447. var html = '<div class="' + CSS.square + ' ' + CSS[color] + '" ' +
  448. 'style="width: ' + size + 'px; height: ' + size + 'px" ' +
  449. 'id="' + id + '">';
  450. if (cfg.showNotation === true) {
  451. }
  452. html += '</div>';
  453. return html;
  454. };
  455. */
  456. function buildBoard(orientation) {
  457. if (orientation !== 'black') {
  458. orientation = 'white';
  459. }
  460. var html = '';
  461. // algebraic notation / orientation
  462. var alpha = deepCopy(COLUMNS);
  463. var row = 7;
  464. if (orientation === 'black') {
  465. alpha.reverse();
  466. row = 1;
  467. }
  468. var squareColor = 'white';
  469. for (var i = 0; i < 7; i++) {
  470. html += '<div class="' + CSS.row + '">';
  471. for (var j = 0; j < 7; j++) {
  472. var square = alpha[j] + row;
  473. html += '<div class="' + CSS.square + ' ' + CSS[squareColor] + ' ' +
  474. 'square-' + square + '" ' +
  475. 'style="width: ' + SQUARE_SIZE + 'px; height: ' + SQUARE_SIZE + 'px" ' +
  476. 'id="' + SQUARE_ELS_IDS[square] + '" ' +
  477. 'data-square="' + square + '">';
  478. if (cfg.showNotation === true) {
  479. // alpha notation
  480. if ((orientation === 'white' && row === 1) ||
  481. (orientation === 'black' && row === 7)) {
  482. html += '<div class="' + CSS.notation + ' ' + CSS.alpha + '">' +
  483. '</div>';
  484. }
  485. // numeric notation
  486. if (j === 0) {
  487. html += '<div class="' + CSS.notation + ' ' + CSS.numeric + '">' +
  488. '</div>';
  489. }
  490. }
  491. html += '</div>'; // end .square
  492. squareColor = (squareColor === 'white' ? 'black' : 'white');
  493. }
  494. html += '<div class="' + CSS.clearfix + '"></div></div>';
  495. squareColor = (squareColor === 'white' ? 'white' : 'black');
  496. if (orientation === 'white') {
  497. row--;
  498. }
  499. else {
  500. row++;
  501. }
  502. }
  503. return html;
  504. }
  505. function buildPieceImgSrc(piece) {
  506. if (typeof cfg.pieceTheme === 'function') {
  507. return cfg.pieceTheme(piece);
  508. }
  509. if (typeof cfg.pieceTheme === 'string') {
  510. return cfg.pieceTheme.replace(/{piece}/g, piece);
  511. }
  512. // NOTE: this should never happen
  513. error(8272, 'Unable to build image source for cfg.pieceTheme.');
  514. return '';
  515. }
  516. function buildPiece(piece, hidden, id) {
  517. var html = '<img src="' + buildPieceImgSrc(piece) + '" ';
  518. if (id && typeof id === 'string') {
  519. html += 'id="' + id + '" ';
  520. }
  521. html += 'alt="" ' +
  522. 'class="' + CSS.piece + '" ' +
  523. 'data-piece="' + piece + '" ' +
  524. 'style="width: ' + SQUARE_SIZE + 'px;' +
  525. 'height: ' + SQUARE_SIZE + 'px;';
  526. if (hidden === true) {
  527. html += 'display:none;';
  528. }
  529. html += '" />';
  530. return html;
  531. }
  532. //------------------------------------------------------------------------------
  533. // Animations
  534. //------------------------------------------------------------------------------
  535. function animateSquareToSquare(src, dest, piece, completeFn) {
  536. // get information about the source and destination squares
  537. var srcSquareEl = $('#' + SQUARE_ELS_IDS[src]);
  538. var srcSquarePosition = srcSquareEl.offset();
  539. var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]);
  540. var destSquarePosition = destSquareEl.offset();
  541. // create the animated piece and absolutely position it
  542. // over the source square
  543. var animatedPieceId = createId();
  544. $('body').append(buildPiece(piece, true, animatedPieceId));
  545. var animatedPieceEl = $('#' + animatedPieceId);
  546. animatedPieceEl.css({
  547. display: '',
  548. position: 'absolute',
  549. top: srcSquarePosition.top,
  550. left: srcSquarePosition.left
  551. });
  552. // remove original piece from source square
  553. srcSquareEl.find('.' + CSS.piece).remove();
  554. // on complete
  555. var complete = function() {
  556. // add the "real" piece to the destination square
  557. destSquareEl.append(buildPiece(piece));
  558. // remove the animated piece
  559. animatedPieceEl.remove();
  560. // run complete function
  561. if (typeof completeFn === 'function') {
  562. completeFn();
  563. }
  564. };
  565. // animate the piece to the destination square
  566. var opts = {
  567. duration: cfg.moveSpeed,
  568. complete: complete
  569. };
  570. animatedPieceEl.animate(destSquarePosition, opts);
  571. }
  572. // execute an array of animations
  573. function doAnimations(a, oldPos, newPos) {
  574. ANIMATION_HAPPENING = true;
  575. var numFinished = 0;
  576. function onFinish() {
  577. numFinished++;
  578. // exit if all the animations aren't finished
  579. if (numFinished !== a.length) return;
  580. drawPositionInstant();
  581. ANIMATION_HAPPENING = false;
  582. // run their onMoveEnd function
  583. if (cfg.hasOwnProperty('onMoveEnd') === true &&
  584. typeof cfg.onMoveEnd === 'function') {
  585. cfg.onMoveEnd(deepCopy(oldPos), deepCopy(newPos));
  586. }
  587. }
  588. for (var i = 0; i < a.length; i++) {
  589. // clear a piece
  590. if (a[i].type === 'clear') {
  591. $('#' + SQUARE_ELS_IDS[a[i].square] + ' .' + CSS.piece)
  592. .fadeOut(cfg.trashSpeed, onFinish);
  593. }
  594. // add a piece (no spare pieces)
  595. if (a[i].type === 'add' && cfg.sparePieces !== true) {
  596. $('#' + SQUARE_ELS_IDS[a[i].square])
  597. .append(buildPiece(a[i].piece, true))
  598. .find('.' + CSS.piece)
  599. .fadeIn(cfg.appearSpeed, onFinish);
  600. }
  601. // add a piece from a spare piece
  602. if (a[i].type === 'add' && cfg.sparePieces === true) {
  603. animateSparePieceToSquare(a[i].piece, a[i].square, onFinish);
  604. }
  605. // move a piece
  606. if (a[i].type === 'move') {
  607. animateSquareToSquare(a[i].source, a[i].destination, a[i].piece,
  608. onFinish);
  609. }
  610. }
  611. }
  612. // returns the distance between two squares
  613. function squareDistance(s1, s2) {
  614. s1 = s1.split('');
  615. var s1x = COLUMNS.indexOf(s1[0]) + 1;
  616. var s1y = parseInt(s1[1], 10);
  617. s2 = s2.split('');
  618. var s2x = COLUMNS.indexOf(s2[0]) + 1;
  619. var s2y = parseInt(s2[1], 10);
  620. var xDelta = Math.abs(s1x - s2x);
  621. var yDelta = Math.abs(s1y - s2y);
  622. if (xDelta >= yDelta) return xDelta;
  623. return yDelta;
  624. }
  625. // returns an array of closest squares from square
  626. function createRadius(square) {
  627. var squares = [];
  628. // calculate distance of all squares
  629. for (var i = 0; i < 7; i++) {
  630. for (var j = 0; j < 7; j++) {
  631. var s = COLUMNS[i] + (j + 1);
  632. // skip the square we're starting from
  633. if (square === s) continue;
  634. squares.push({
  635. square: s,
  636. distance: squareDistance(square, s)
  637. });
  638. }
  639. }
  640. // sort by distance
  641. squares.sort(function(a, b) {
  642. return a.distance - b.distance;
  643. });
  644. // just return the square code
  645. var squares2 = [];
  646. for (var i = 0; i < squares.length; i++) {
  647. squares2.push(squares[i].square);
  648. }
  649. return squares2;
  650. }
  651. // returns the square of the closest instance of piece
  652. // returns false if no instance of piece is found in position
  653. function findClosestPiece(position, piece, square) {
  654. // create array of closest squares from square
  655. var closestSquares = createRadius(square);
  656. // search through the position in order of distance for the piece
  657. for (var i = 0; i < closestSquares.length; i++) {
  658. var s = closestSquares[i];
  659. if (position.hasOwnProperty(s) === true && position[s] === piece) {
  660. return s;
  661. }
  662. }
  663. return false;
  664. }
  665. // calculate an array of animations that need to happen in order to get
  666. // from pos1 to pos2
  667. function calculateAnimations(pos1, pos2) {
  668. // make copies of both
  669. pos1 = deepCopy(pos1);
  670. pos2 = deepCopy(pos2);
  671. var animations = [];
  672. var squaresMovedTo = {};
  673. // remove pieces that are the same in both positions
  674. for (var i in pos2) {
  675. if (pos2.hasOwnProperty(i) !== true) continue;
  676. if (pos1.hasOwnProperty(i) === true && pos1[i] === pos2[i]) {
  677. delete pos1[i];
  678. delete pos2[i];
  679. }
  680. }
  681. // find all the "move" animations
  682. for (var i in pos2) {
  683. if (pos2.hasOwnProperty(i) !== true) continue;
  684. var closestPiece = findClosestPiece(pos1, pos2[i], i);
  685. if (closestPiece !== false) {
  686. animations.push({
  687. type: 'move',
  688. source: closestPiece,
  689. destination: i,
  690. piece: pos2[i]
  691. });
  692. delete pos1[closestPiece];
  693. delete pos2[i];
  694. squaresMovedTo[i] = true;
  695. }
  696. }
  697. // add pieces to pos2
  698. for (var i in pos2) {
  699. if (pos2.hasOwnProperty(i) !== true) continue;
  700. animations.push({
  701. type: 'add',
  702. square: i,
  703. piece: pos2[i]
  704. })
  705. delete pos2[i];
  706. }
  707. // clear pieces from pos1
  708. for (var i in pos1) {
  709. if (pos1.hasOwnProperty(i) !== true) continue;
  710. // do not clear a piece if it is on a square that is the result
  711. // of a "move", ie: a piece capture
  712. if (squaresMovedTo.hasOwnProperty(i) === true) continue;
  713. animations.push({
  714. type: 'clear',
  715. square: i,
  716. piece: pos1[i]
  717. });
  718. delete pos1[i];
  719. }
  720. return animations;
  721. }
  722. //------------------------------------------------------------------------------
  723. // Control Flow
  724. //------------------------------------------------------------------------------
  725. function drawPositionInstant() {
  726. // clear the board
  727. boardEl.find('.' + CSS.piece).remove();
  728. // add the pieces
  729. for (var i in CURRENT_POSITION) {
  730. if (CURRENT_POSITION.hasOwnProperty(i) !== true) continue;
  731. $('#' + SQUARE_ELS_IDS[i]).append(buildPiece(CURRENT_POSITION[i]));
  732. }
  733. }
  734. function drawBoard() {
  735. boardEl.html(buildBoard(CURRENT_ORIENTATION));
  736. drawPositionInstant();
  737. }
  738. // given a position and a set of moves, return a new position
  739. // with the moves executed
  740. function calculatePositionFromMoves(position, moves) {
  741. position = deepCopy(position);
  742. for (var i in moves) {
  743. if (moves.hasOwnProperty(i) !== true) continue;
  744. // skip the move if the position doesn't have a piece on the source square
  745. if (position.hasOwnProperty(i) !== true) continue;
  746. var piece = position[i];
  747. delete position[i];
  748. position[moves[i]] = piece;
  749. }
  750. return position;
  751. }
  752. function setCurrentPosition(position) {
  753. var oldPos = deepCopy(CURRENT_POSITION);
  754. var newPos = deepCopy(position);
  755. var oldFen = objToFen(oldPos);
  756. var newFen = objToFen(newPos);
  757. // do nothing if no change in position
  758. if (oldFen === newFen) return;
  759. // run their onChange function
  760. if (cfg.hasOwnProperty('onChange') === true &&
  761. typeof cfg.onChange === 'function') {
  762. cfg.onChange(oldPos, newPos);
  763. }
  764. // update state
  765. CURRENT_POSITION = position;
  766. }
  767. function isXYOnSquare(x, y) {
  768. for (var i in SQUARE_ELS_OFFSETS) {
  769. if (SQUARE_ELS_OFFSETS.hasOwnProperty(i) !== true) continue;
  770. var s = SQUARE_ELS_OFFSETS[i];
  771. if (x >= s.left && x < s.left + SQUARE_SIZE &&
  772. y >= s.top && y < s.top + SQUARE_SIZE) {
  773. return i;
  774. }
  775. }
  776. return 'offboard';
  777. }
  778. // records the XY coords of every square into memory
  779. function captureSquareOffsets() {
  780. SQUARE_ELS_OFFSETS = {};
  781. for (var i in SQUARE_ELS_IDS) {
  782. if (SQUARE_ELS_IDS.hasOwnProperty(i) !== true) continue;
  783. SQUARE_ELS_OFFSETS[i] = $('#' + SQUARE_ELS_IDS[i]).offset();
  784. }
  785. }
  786. function removeSquareHighlights() {
  787. boardEl.find('.' + CSS.square)
  788. .removeClass(CSS.highlight1 + ' ' + CSS.highlight2);
  789. }
  790. //------------------------------------------------------------------------------
  791. // Public Methods
  792. //------------------------------------------------------------------------------
  793. // clear the board
  794. widget.clear = function(useAnimation) {
  795. widget.position({}, useAnimation);
  796. };
  797. /*
  798. // get or set config properties
  799. // TODO: write this, GitHub Issue #1
  800. widget.config = function(arg1, arg2) {
  801. // get the current config
  802. if (arguments.length === 0) {
  803. return deepCopy(cfg);
  804. }
  805. };
  806. */
  807. // remove the widget from the page
  808. widget.destroy = function() {
  809. // remove markup
  810. containerEl.html('');
  811. draggedPieceEl.remove();
  812. // remove event handlers
  813. containerEl.unbind();
  814. };
  815. // flip orientation
  816. widget.flip = function() {
  817. widget.orientation('flip');
  818. };
  819. /*
  820. // TODO: write this, GitHub Issue #5
  821. widget.highlight = function() {
  822. };
  823. */
  824. // move pieces
  825. widget.move = function() {
  826. // no need to throw an error here; just do nothing
  827. if (arguments.length === 0) return;
  828. var useAnimation = true;
  829. // collect the moves into an object
  830. var moves = {};
  831. for (var i = 0; i < arguments.length; i++) {
  832. // any "false" to this function means no animations
  833. if (arguments[i] === false) {
  834. useAnimation = false;
  835. continue;
  836. }
  837. // skip invalid arguments
  838. if (validMove(arguments[i]) !== true) {
  839. error(2826, 'Invalid move passed to the move method.', arguments[i]);
  840. continue;
  841. }
  842. var tmp = arguments[i].split('-');
  843. moves[tmp[0]] = tmp[1];
  844. }
  845. // calculate position from moves
  846. var newPos = calculatePositionFromMoves(CURRENT_POSITION, moves);
  847. // update the board
  848. widget.position(newPos, useAnimation);
  849. // block the square
  850. for (var m in moves) {
  851. var el = document.getElementById(SQUARE_ELS_IDS[m]);
  852. el.classList.add("blocked");
  853. }
  854. // return the new position object
  855. return newPos;
  856. };
  857. widget.position = function(position, useAnimation) {
  858. // no arguments, return the current position
  859. if (arguments.length === 0) {
  860. return deepCopy(CURRENT_POSITION);
  861. }
  862. // get position as FEN
  863. if (typeof position === 'string' && position.toLowerCase() === 'fen') {
  864. return objToFen(CURRENT_POSITION);
  865. }
  866. // default for useAnimations is true
  867. if (useAnimation !== false) {
  868. useAnimation = true;
  869. }
  870. // start position
  871. if (typeof position === 'string' && position.toLowerCase() === 'start') {
  872. position = deepCopy(START_POSITION);
  873. }
  874. // convert FEN to position object
  875. if (validFen(position) === true) {
  876. position = fenToObj(position);
  877. }
  878. // validate position object
  879. if (validPositionObject(position) !== true) {
  880. error(6482, 'Invalid value passed to the position method.', position);
  881. return;
  882. }
  883. if (useAnimation === true) {
  884. // start the animations
  885. doAnimations(calculateAnimations(CURRENT_POSITION, position),
  886. CURRENT_POSITION, position);
  887. // set the new position
  888. setCurrentPosition(position);
  889. }
  890. // instant update
  891. else {
  892. setCurrentPosition(position);
  893. drawPositionInstant();
  894. }
  895. };
  896. widget.resize = function() {
  897. // calulate the new square size
  898. SQUARE_SIZE = calculateSquareSize();
  899. // set board width
  900. boardEl.css('width', (SQUARE_SIZE * 7) + 'px');
  901. // redraw the board
  902. drawBoard();
  903. };
  904. widget.finalize = function(winCell, lossCell) {
  905. var el;
  906. el = document.getElementById(SQUARE_ELS_IDS[winCell]);
  907. el.classList.add("win");
  908. el = document.getElementById(SQUARE_ELS_IDS[lossCell]);
  909. el.classList.add("lose");
  910. };
  911. // set the starting position
  912. widget.start = function(useAnimation) {
  913. widget.position('start', useAnimation);
  914. };
  915. //------------------------------------------------------------------------------
  916. // Browser Events
  917. //------------------------------------------------------------------------------
  918. function stopDefault(e) {
  919. e.preventDefault();
  920. }
  921. //------------------------------------------------------------------------------
  922. // Initialization
  923. //------------------------------------------------------------------------------
  924. function initDom() {
  925. // build board and save it in memory
  926. containerEl.html(buildBoardContainer());
  927. boardEl = containerEl.find('.' + CSS.board);
  928. // get the border size
  929. BOARD_BORDER_SIZE = parseInt(boardEl.css('borderLeftWidth'), 10);
  930. // set the size and draw the board
  931. widget.resize();
  932. }
  933. function init() {
  934. if (checkDeps() !== true ||
  935. expandConfig() !== true) return;
  936. // create unique IDs for all the elements we will create
  937. createElIds();
  938. initDom();
  939. }
  940. // go time
  941. init();
  942. // return the widget object
  943. return widget;
  944. }; // end window.ChessBoard
  945. // expose util functions
  946. window.ChessBoard.fenToObj = fenToObj;
  947. window.ChessBoard.objToFen = objToFen;
  948. })(); // end anonymous wrapper