From 47b83f8587cb1267fba918df60112c919ea00942 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 29 Aug 2018 23:24:45 -0500 Subject: [PATCH] Add files via upload --- ChessEngine.js | 193 +++++ bB.png | Bin 0 -> 1405 bytes bK.png | Bin 0 -> 3009 bytes bN.png | Bin 0 -> 1875 bytes bP.png | Bin 0 -> 777 bytes bQ.png | Bin 0 -> 2648 bytes bR.png | Bin 0 -> 748 bytes chessboard-0.3.0.css | 70 ++ chessboard-0.3.0.js | 1714 ++++++++++++++++++++++++++++++++++++++++++ wB.png | Bin 0 -> 2374 bytes wK.png | Bin 0 -> 2823 bytes wN.png | Bin 0 -> 2388 bytes wP.png | Bin 0 -> 1571 bytes wQ.png | Bin 0 -> 3812 bytes wR.png | Bin 0 -> 1097 bytes 15 files changed, 1977 insertions(+) create mode 100644 ChessEngine.js create mode 100644 bB.png create mode 100644 bK.png create mode 100644 bN.png create mode 100644 bP.png create mode 100644 bQ.png create mode 100644 bR.png create mode 100644 chessboard-0.3.0.css create mode 100644 chessboard-0.3.0.js create mode 100644 wB.png create mode 100644 wK.png create mode 100644 wN.png create mode 100644 wP.png create mode 100644 wQ.png create mode 100644 wR.png diff --git a/ChessEngine.js b/ChessEngine.js new file mode 100644 index 0000000..b9b98db --- /dev/null +++ b/ChessEngine.js @@ -0,0 +1,193 @@ +const maxBoardLength = 8; +window.onload = function(){chess_main();}; + + +class ChessBot{ + constructor(difficulty, color){ + this.difficulty = difficulty; + this.color = color; + } + + makeMove(game, board, source, target){ + let nextMove = undefined; + if (this.difficulty === 'Random'){ + nextMove = this.determineSimplestBotMove(game, source, target); + } else if (this.difficulty === 'Easy') { + nextMove = this.determineNextBestBotMoveOneMoveDeep(game, source, target); + } else if (this.difficulty === 'Medium'){ + let depth = Math.floor(Math.random() * 3 + 2); + console.clear(); + console.log(depth + " moves ahead."); + nextMove = this.determineBestBotMove(depth, game, true); + } + + if (nextMove !== undefined) { + game.move(nextMove); + board.position(game.fen()); + }else { + throw "difficulty not recognized" + this.difficulty; + } + } + + determineSimplestBotMove(game, source, target){ + let possibleMoves = game.moves(); + + if (possibleMoves.length === 0) return; + + let randomIndex = Math.floor(Math.random() * possibleMoves.length); + return possibleMoves[randomIndex]; + } + + getWhitePieceWeight(piece){ + if (piece.type === 'p'){ + return 10; + } else if (piece.type === 'r'){ + return 50; + } else if (piece.type === 'n'){ + return 30; + } else if (piece.type === 'b'){ + return 35; + } else if (piece.type === 'q'){ + return 90; + } else if (piece.type === 'k'){ + return 900; + } + throw "Unknown piece type: " + piece.type; + } + + getPieceValue(piece){ + if(piece === null){ + return 0; + } + return piece.color === 'w' ? this.getWhitePieceWeight(piece) : -this.getWhitePieceWeight(piece); + } + + evaluateBoardForWhite(game){ + let totalEvaluation = 0; + for (let i = 0; i < maxBoardLength; i++){ + for (let j = 0; j < maxBoardLength; j++){ + totalEvaluation += this.getPieceValue(game.get(String.fromCharCode(97 + i) + (j + 1))); + } + } + return totalEvaluation; + } + + evaluateBoardForBlack(game){return -this.evaluateBoardForWhite(game);} + + determineNextBestBotMoveOneMoveDeep(game, source, target) { + let bestMove = null; + let bestValue = -9999; + let boardValue = 0; + let possibleMoves = game.moves(); + + for (let i = 0; i < possibleMoves.length; i++) { + let newGameMove = possibleMoves[i]; + game.move(newGameMove); + + if (this.color === "w") { + boardValue = this.evaluateBoardForWhite(game); + } else if (this.color === "b") { + boardValue = this.evaluateBoardForBlack(game); + } else { + throw "Unknown bot team color: " + this.color; + } + game.undo(); + + if (boardValue > bestValue) { + bestValue = boardValue; + bestMove = newGameMove; + } + } + + if (bestValue === 0) { + return this.determineSimplestBotMove(game, source, target); + } else { + return bestMove; + } + } + + determineBestBotMove(depth, game, isMaximizingPlayer){ + let possibleMoves = game.moves(); + let bestValue = -9999; + let bestMove; + + for (let i = 0; i < possibleMoves.length; i++){ + game.move(possibleMoves[i]) + let currentValue = this.mini_max_AB(depth - 1, game, -10000, 10000, !isMaximizingPlayer); + game.undo(); + if(currentValue >= bestValue){ + bestMove = possibleMoves[i]; + bestValue = currentValue; + } + } + return bestMove; + } + + mini_max_AB(depth, game, alpha, beta, isMaximizingPlayer){ + if (depth === 0 && this.color === 'w'){ + return this.evaluateBoardForWhite(game); + } else if (depth === 0 && this.color === 'b') { + return this.evaluateBoardForBlack(game); + } + + let possibleMoves = game.moves(); + let bestValue = 9999 * (isMaximizingPlayer ? -1 : 1); + for (let i = 0; i < possibleMoves.length; i++){ + game.move(possibleMoves[i]); + if (isMaximizingPlayer){ + bestValue = Math.max(bestValue, this.mini_max_AB(depth-1, game, alpha, beta, !isMaximizingPlayer)); + alpha = Math.max(alpha, bestValue); + } else { + bestValue = Math.min(bestValue, this.mini_max_AB(depth-1, game, alpha, beta, !isMaximizingPlayer)); + beta = Math.min(beta, bestValue); + } + game.undo(); + if (beta <= alpha){ + return bestValue; + } + } + return bestValue; + } +} + +function chess_main() { + let game = new Chess(); + let board = ChessBoard('board'); + let BBot = new ChessBot('Medium', 'b'); + setupChess(game, board, BBot); +} + +function setupChess(game, board, bot){ + let onDragStart = function(source, piece, position, orientation) { + if (game.in_checkmate() === true || game.in_draw() === true || + piece.search(/^b/) !== -1) { + return false; + } + }; + + let onDrop = function(source, target) { + let move = game.move({ + from: source, + to: target, + promotion: 'q' // NOTE: always promote to a queen for example simplicity + }); + + if (move === null) return 'snapback'; + + window.setTimeout(bot.makeMove(game, board, source, target), 250); + }; + + let onSnapEnd = function() { + board.position(game.fen()); + }; + + let cfg = { + draggable: true, + dropOffBoard: 'snapback', + position: 'start', + onDragStart: onDragStart, + onDrop: onDrop, + onSnapEnd: onSnapEnd + }; + board = ChessBoard('board', cfg); +} diff --git a/bB.png b/bB.png new file mode 100644 index 0000000000000000000000000000000000000000..be3007dd0cdc7662268e103458b30a035da1cebf GIT binary patch literal 1405 zcmV-@1%mpCP);OAg&;29>pXtLFL9VGS_o&_2gy))=`i$DX3vw-9Gh)}RQ7JcwuWkdbRXt|A`xA>c4EW@MX> zj1K`bMy|`TiE&}qk z)5K?tT>s;C;y#C_DqWhf#67pwvT!jB9HSn^b1L&UY}D79cyZ!hpTfBbxfeETrsfTo zDW224pVh+TdWo{)C`Y)3gggu%T4E{j;}FCv7V#rpMSR#zUWI`}!j4;@#o3 zwq!Dyxf++t==OPfWGs*%U_4=RzH(`}WqAcL=rA~|k z8bl;wh$ODZe5|BZM;X(Y;=zPth-6yGt$ruZ0ZgQw$T8d!)h(;@rPja_mvT-ukYl(u zs?GS<5H=0`@|P$NBKsGCfS*QLz?~s(UwK`PycA^t`CY|i6-2asO<*4~(nwcPz*lwH zOyD`#=M@yu_Hk5blorehk!O_$wBqr2etHrF*_N1}&3aa7n1s2Jc$^=wp`pRbX0!Q? zf^xw&*cRJ_c;ARZQo5G7nMF*cQgUo;Oa=wE#Wo>cLTHTVxt7$`)ybx&CfU{1CA+)3 z%OcxgTWk|G@Ht(@yARWfFb5I~K9ahL$MeAh6&Q)t7fM>?%RDUH^ZtS+=Mk>cNx)rI zSjxSui+C~W>_5P$Ysv6ov9F$kz?Yr25HN<*lqbT}EQgl6svu9+Hr%txLo=goZiOOR&&?PZU1ak|MYyb7l^<6GG!O zUsX)rSl|~ve_;JO<3qd|+{5<{VFMxZqVewU3gVr_)x>+TsJ|bZn-3D#ibCe-dTAJ#4RquQl+UL@cWj+ls}qQ@x6~8_nh*F-!cKc!KyX@f7g~ z;u+#k#B;>oh$F-?G&xy9Q+6togUu26I0v6+5W^|NbOJGEvAMyqKU-C93njH_WNeCt*&$TPie{tCooB3#mtf7LOCfFK8;> zLZ3-&4stj7of;6YSj2aO+|}j}oF}i)8Rt-D9IFA9~G_GA#Y&zMRQ2aT?G3jCO<1$YN-Hcm_$1rNmBAD+);Xe%ck8oZgZpVBqMSKWM zVSdJQwJpSL#3qGX(M52%yNJtNVpxVUDcrNQ$ksOa*Z`ku9P1F<4#c_#v44+eu}9=% z=trP|Z1_lNO1_lNO2I1mAaAmM>MWmh600000 LNkvXXu0mjfilMA( literal 0 HcmV?d00001 diff --git a/bK.png b/bK.png new file mode 100644 index 0000000000000000000000000000000000000000..de9880ce6390d6a53464f1b028bff7be0fd0f131 GIT binary patch literal 3009 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 DNo%uI literal 0 HcmV?d00001 diff --git a/bN.png b/bN.png new file mode 100644 index 0000000000000000000000000000000000000000..e31a6d0224819791210e91218fc99b7cd3b848ed GIT binary patch literal 1875 zcmV-Z2dwysP)9wVNxLjD`@*iP6__$6VA8`tL&-X#RW zUP5D_3pg!{fgB^uc5!V9p(tNV|Gp5&;zh}#i3&mRJj2PgnS>Jx@#yvSfh=IOEEuS) zYKSmaaa{!A4~pxQ{4vmn@SiF{sCCuL0>==(p%PfhDZ)^~NW!o{6tLqymZi2z(Zy#! z%lA1-SS~L#j}it2f`CuC5C&X`M>(%N&&tYzef##^$h?u1g{eeo%(*%pyZ{SquvcgUrlKsIIPto}M0{~GzX|xd+VWPeTnQG71|}zqobjzsYw#CySp0~m(fM`;oP82 zZGH9i^^$O+D2*(Lii*-?NRv4b8X6j;u1}sk2@w$yzwz!f78Cxfe2Mkz*MkzBot>J_ zi$Os_*r&8Y@7}!|&wt@nJdE3Qsb2tBLnYGF(?RRS1y|{tH*doH{Jgkk%^Ga`oJRp) zC0tNHd~4?}Qpu;v0H3knLPDTMfpq8zX-;z>M=lnglY`8ICc2#t-6uxHO6 zAgiHI#6#j2ck0wB>5DX*&Di!WX?utl0V`cS{E8JTd}et{CMPGu?c29ucz77%;^MHa zln3!*eJ#vVSXWmkt;&ZE9m4ZIfrFgO7g}uByhWqYh>HnayLL@P=bp6X`BJ~~C=smv@?6opVFtPUR>}+XoJUKZDX=!QL z_9n}8C}!Edak8(JfX7~NQ$BXm?*>+LZc6k$(}ExzT4cYhRPPGX4GiQjkGTO)$&%e&+@8x)lE1V!?_D)&d?pcu*4X z)~#Do`hm1nvvgw`Jbd8K1owWrfWeB(TI_gm7lOb_FRVS3Lv+kw5|&?W)h)RHyN^1- zz2ZxcSyWWy@FcLVMvDOJnI#NYdORYyUnNzhWsnw~X^NdYcVgS$TI`2w2?u3sqz`jP z6?RgFqTCG?y{z8pux3U^hC{%;d-qC}3|d8H(4scW3F-pDUbxfi5PVpenWuQT8-)38?k({PSnyS@n8IqcN|mpqeVyP(DAH=daK^;M zM2CPUPMnb9go6E7Q+|Q%EOH)8Ar>A{U_GH+ep2WU;yo{~cs@;wDg``u?i|(-V*4w+ zi}x_$Fb^qCOlwkLh#!kGNxPYM0lyYFNysbbK%31*1>DcGc&`Z@RWm3BymaXj74U6d z#WQ#q(P1hemLJRY+f}>@xJ}-%a8pY|>ts|GXyH}BPkFLFWJ*fPJ!uJ9z#1OKi;*KT zTnUIp?-ZCHmfe{5c@%IR>->hN(U_K&7V1d9$D@Ex^5Xjms_K1;)2C0k zj|cA9vBM$Y@#DwoUie)e1$>6^55fLVBzWQEu3fukF|m#%9#*S$E-WkzW1I(g6fjn{ z!sj^Rvu3k-^T5DB!QkLvfyH9kLIlLP<|Bd*9XfRA(4j+z4jnq!;y-|v529HMk23%O N002ovPDHLkV1nn}h0*{3 literal 0 HcmV?d00001 diff --git a/bP.png b/bP.png new file mode 100644 index 0000000000000000000000000000000000000000..afa0c9d4459e4d04648ca413ba90690bfdd49a02 GIT binary patch literal 777 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 l`)^ z-n=PRR8&Y=QBY6-yLRn@ZQHiN`t|E!<;sAcp zw%QWGCeI%Jk3K#=;OpxPetv$>x@|x}z;l$ZV;L-qHWmf^#$AssUBz<`-r2+glP6CG z>_T(r&V{(RIEasrhgGXq!P>QJVbi8fuzmY>$jZus-Me?gzJ2>3CnpCEA3h96j~SgBFX6i=n){9A`ir z#HFPO&Xg|-=o{<6x~^WmD%Fi`z_y^^(b3Vi|D)|m85tRE>cCoCTcNtTS_%f25@~5^ zkdl%DD^{$K)@n0n&V)&mCP8Rus6?D)wJ3j&Wzh!LnCJt2q0d8y4oP)jU0A0YnVFgY z5%}M%^~l)wFE;yP_Y&ER2N4kw9-lmLse^4{OB)f}CJerjZ%cfnx1Ga5AJ=dbCQN`S zQ>IA5PM(V}NuxNxC#9lytNSe|u9X$^>eP$cv_ zapFW6GiHo5(Fn$G-M?kKu8Z3v9K5khZP~H~_UzdM`T6;9>eMMHEiHu$7cRi%%a@_9 zt`2V8xPgmpX*-6yjE4^&O1p_CPo7BQSZ{Byw4)dp7?9Z4*C$=a@8ul~?nv(3xdXRu z-GYXO2FWjO>n>ir2xVnuQdcf4EQGy#_d;rFDkLT*!i*U+ASfsZp8#grO1<3N+@D^% zcC8n;R!^Tk1yR)8$w^x<9N^hj52~uF;M}=$P*hYT1)7(a2S<(^k!D<+Z87ld>}=^@ z{6AhtVmZ7qY%M3<7~EyxB-PZ^B#CM?!QhR$efze!dGqFv?4;ia)~!l0yBZrCMHFP` z&Yh5)oGf)Y?5^ytyLH^dbz^vVIK;-r!kRT}aPoi?Cr-fi>(`}>UA}yIt(^`NLcH-L znk~eW9RXcC58P}Q0r4H>Prd9|A72ewX17`RufpI%>V9E(U(Fa!AN|zEPw1(lt$@F; z@GHhYPy@?IR2;DJ?WY!c2HX1rGpm>CXrVf5sSRb+rajbkz2f&{@Gp_Z({Vq(Iu^~i zKp6b}gH?=4!r<>@;qT1gd(Zf)?{3Bqg~7i%!h_FT;$gb>A(hD{n^f|VL_XuGj%cbg zoZ8?kyyUPnW0000+NjA?5IGAHvP4t z54%KWsa~*up#S25g4PyUJ-=B#vU8SfOkVC3u;J1>CCN>xZx>V_Sh(vMyUFU{q&A(T z%a1H=@4r>w>!lpyyYBwP?~T)?JD%6QuQd^xTrI;6v+VaXtCeeiy8Wc6`Rl~%4ZC%_?Pl1m|0#bk z^7R^V?ErQbHRi&f$MXc79yDCv;r+Zw?g6KO{)aj8(mb3yc@;lQyRI|;dChg!RlNKE z2Rz^KOH@NqfTL01M}LIQvs-Etth*oXd|r8f`Q??<_)3?oyAnC+!1AR*FSp!&`>Jlg ze_+iNk^RBzuU~#z^mB@p_ivL;)4i7N+qG`>cNAB`UCr7Rwbo5)mgnY` z%;g)qY$tQOpL?4le&-}t+^m}H?YFCJM3nTCehM|;g7cR zGA+F`RWxmJzeVFD=0;#pu4Yve;?#ZdEzbUm=b{7K&po<#$IEQ?)x!@1Zt*^=Q-AeL z=bfkBqwE`8`967bj@It5w>>Ys`)K2?z~f8Lnb+t}{M^W$ak}{Ubs@{GvO*EfV88O5 ze8_iV_VWU1rp*0s-6soA?ptd4F#E=>dx^Ko-DaPg7dgu!o*9yi7XDzq)Gu?~A%9jq ONWjz8&t;ucLK6U+JX?nV literal 0 HcmV?d00001 diff --git a/chessboard-0.3.0.css b/chessboard-0.3.0.css new file mode 100644 index 0000000..e987b52 --- /dev/null +++ b/chessboard-0.3.0.css @@ -0,0 +1,70 @@ +/*! + * chessboard.js v0.3.0 + * + * Copyright 2013 Chris Oakman + * Released under the MIT license + * https://github.com/oakmac/chessboardjs/blob/master/LICENSE + * + * Date: 10 Aug 2013 + */ + +/* clearfix */ +.clearfix-7da63 { + clear: both; +} + +/* board */ +.board-b72b1 { + border: 2px solid #404040; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +/* square */ +.square-55d63 { + float: left; + position: relative; + + /* disable any native browser highlighting */ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* white square */ +.white-1e1d7 { + background-color: #f0d9b5; + color: #b58863; +} + +/* black square */ +.black-3c85d { + background-color: #b58863; + color: #f0d9b5; +} + +/* highlighted square */ +.highlight1-32417, .highlight2-9c5d2 { + -webkit-box-shadow: inset 0 0 3px 3px yellow; + -moz-box-shadow: inset 0 0 3px 3px yellow; + box-shadow: inset 0 0 3px 3px yellow; +} + +/* notation */ +.notation-322f9 { + cursor: default; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + position: absolute; +} +.alpha-d2270 { + bottom: 1px; + right: 3px; +} +.numeric-fc462 { + top: 2px; + left: 2px; +} \ No newline at end of file diff --git a/chessboard-0.3.0.js b/chessboard-0.3.0.js new file mode 100644 index 0000000..8f1ac2d --- /dev/null +++ b/chessboard-0.3.0.js @@ -0,0 +1,1714 @@ +/*! + * chessboard.js v0.3.0 + * + * Copyright 2013 Chris Oakman + * Released under the MIT license + * http://chessboardjs.com/license + * + * Date: 10 Aug 2013 + */ + +// start anonymous scope +;(function() { +'use strict'; + +//------------------------------------------------------------------------------ +// Chess Util Functions +//------------------------------------------------------------------------------ +var COLUMNS = 'abcdefgh'.split(''); + +function validMove(move) { + // move should be a string + if (typeof move !== 'string') return false; + + // move should be in the form of "e2-e4", "f6-d5" + var tmp = move.split('-'); + if (tmp.length !== 2) return false; + + return (validSquare(tmp[0]) === true && validSquare(tmp[1]) === true); +} + +function validSquare(square) { + if (typeof square !== 'string') return false; + return (square.search(/^[a-h][1-8]$/) !== -1); +} + +function validPieceCode(code) { + if (typeof code !== 'string') return false; + return (code.search(/^[bw][KQRNBP]$/) !== -1); +} + +// TODO: this whole function could probably be replaced with a single regex +function validFen(fen) { + if (typeof fen !== 'string') return false; + + // cut off any move, castling, etc info from the end + // we're only interested in position information + fen = fen.replace(/ .+$/, ''); + + // FEN should be 8 sections separated by slashes + var chunks = fen.split('/'); + if (chunks.length !== 8) return false; + + // check the piece sections + for (var i = 0; i < 8; i++) { + if (chunks[i] === '' || + chunks[i].length > 8 || + chunks[i].search(/[^kqrbnpKQRNBP1-8]/) !== -1) { + return false; + } + } + + return true; +} + +function validPositionObject(pos) { + if (typeof pos !== 'object') return false; + + for (var i in pos) { + if (pos.hasOwnProperty(i) !== true) continue; + + if (validSquare(i) !== true || validPieceCode(pos[i]) !== true) { + return false; + } + } + + return true; +} + +// convert FEN piece code to bP, wK, etc +function fenToPieceCode(piece) { + // black piece + if (piece.toLowerCase() === piece) { + return 'b' + piece.toUpperCase(); + } + + // white piece + return 'w' + piece.toUpperCase(); +} + +// convert bP, wK, etc code to FEN structure +function pieceCodeToFen(piece) { + var tmp = piece.split(''); + + // white piece + if (tmp[0] === 'w') { + return tmp[1].toUpperCase(); + } + + // black piece + return tmp[1].toLowerCase(); +} + +// convert FEN string to position object +// returns false if the FEN string is invalid +function fenToObj(fen) { + if (validFen(fen) !== true) { + return false; + } + + // cut off any move, castling, etc info from the end + // we're only interested in position information + fen = fen.replace(/ .+$/, ''); + + var rows = fen.split('/'); + var position = {}; + + var currentRow = 8; + for (var i = 0; i < 8; i++) { + var row = rows[i].split(''); + var colIndex = 0; + + // loop through each character in the FEN section + for (var j = 0; j < row.length; j++) { + // number / empty squares + if (row[j].search(/[1-8]/) !== -1) { + var emptySquares = parseInt(row[j], 10); + colIndex += emptySquares; + } + // piece + else { + var square = COLUMNS[colIndex] + currentRow; + position[square] = fenToPieceCode(row[j]); + colIndex++; + } + } + + currentRow--; + } + + return position; +} + +// position object to FEN string +// returns false if the obj is not a valid position object +function objToFen(obj) { + if (validPositionObject(obj) !== true) { + return false; + } + + var fen = ''; + + var currentRow = 8; + for (var i = 0; i < 8; i++) { + for (var j = 0; j < 8; j++) { + var square = COLUMNS[j] + currentRow; + + // piece exists + if (obj.hasOwnProperty(square) === true) { + fen += pieceCodeToFen(obj[square]); + } + + // empty space + else { + fen += '1'; + } + } + + if (i !== 7) { + fen += '/'; + } + + currentRow--; + } + + // squeeze the numbers together + // haha, I love this solution... + fen = fen.replace(/11111111/g, '8'); + fen = fen.replace(/1111111/g, '7'); + fen = fen.replace(/111111/g, '6'); + fen = fen.replace(/11111/g, '5'); + fen = fen.replace(/1111/g, '4'); + fen = fen.replace(/111/g, '3'); + fen = fen.replace(/11/g, '2'); + + return fen; +} + +window['ChessBoard'] = window['ChessBoard'] || function(containerElOrId, cfg) { +'use strict'; + +cfg = cfg || {}; + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +var MINIMUM_JQUERY_VERSION = '1.7.0', + START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + START_POSITION = fenToObj(START_FEN); + +// use unique class names to prevent clashing with anything else on the page +// and simplify selectors +var CSS = { + alpha: 'alpha-d2270', + black: 'black-3c85d', + board: 'board-b72b1', + chessboard: 'chessboard-63f37', + clearfix: 'clearfix-7da63', + highlight1: 'highlight1-32417', + highlight2: 'highlight2-9c5d2', + notation: 'notation-322f9', + numeric: 'numeric-fc462', + piece: 'piece-417db', + row: 'row-5277c', + sparePieces: 'spare-pieces-7492f', + sparePiecesBottom: 'spare-pieces-bottom-ae20f', + sparePiecesTop: 'spare-pieces-top-4028b', + square: 'square-55d63', + white: 'white-1e1d7' +}; + +//------------------------------------------------------------------------------ +// Module Scope Variables +//------------------------------------------------------------------------------ + +// DOM elements +var containerEl, + boardEl, + draggedPieceEl, + sparePiecesTopEl, + sparePiecesBottomEl; + +// constructor return object +var widget = {}; + +//------------------------------------------------------------------------------ +// Stateful +//------------------------------------------------------------------------------ + +var ANIMATION_HAPPENING = false, + BOARD_BORDER_SIZE = 2, + CURRENT_ORIENTATION = 'white', + CURRENT_POSITION = {}, + SQUARE_SIZE, + DRAGGED_PIECE, + DRAGGED_PIECE_LOCATION, + DRAGGED_PIECE_SOURCE, + DRAGGING_A_PIECE = false, + SPARE_PIECE_ELS_IDS = {}, + SQUARE_ELS_IDS = {}, + SQUARE_ELS_OFFSETS; + +//------------------------------------------------------------------------------ +// JS Util Functions +//------------------------------------------------------------------------------ + +// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript +function createId() { + return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function(c) { + var r = Math.random() * 16 | 0; + return r.toString(16); + }); +} + +function deepCopy(thing) { + return JSON.parse(JSON.stringify(thing)); +} + +function parseSemVer(version) { + var tmp = version.split('.'); + return { + major: parseInt(tmp[0], 10), + minor: parseInt(tmp[1], 10), + patch: parseInt(tmp[2], 10) + }; +} + +// returns true if version is >= minimum +function compareSemVer(version, minimum) { + version = parseSemVer(version); + minimum = parseSemVer(minimum); + + var versionNum = (version.major * 10000 * 10000) + + (version.minor * 10000) + version.patch; + var minimumNum = (minimum.major * 10000 * 10000) + + (minimum.minor * 10000) + minimum.patch; + + return (versionNum >= minimumNum); +} + +//------------------------------------------------------------------------------ +// Validation / Errors +//------------------------------------------------------------------------------ + +function error(code, msg, obj) { + // do nothing if showErrors is not set + if (cfg.hasOwnProperty('showErrors') !== true || + cfg.showErrors === false) { + return; + } + + var errorText = 'ChessBoard Error ' + code + ': ' + msg; + + // print to console + if (cfg.showErrors === 'console' && + typeof console === 'object' && + typeof console.log === 'function') { + console.log(errorText); + if (arguments.length >= 2) { + console.log(obj); + } + return; + } + + // alert errors + if (cfg.showErrors === 'alert') { + if (obj) { + errorText += '\n\n' + JSON.stringify(obj); + } + window.alert(errorText); + return; + } + + // custom function + if (typeof cfg.showErrors === 'function') { + cfg.showErrors(code, msg, obj); + } +} + +// check dependencies +function checkDeps() { + // if containerId is a string, it must be the ID of a DOM node + if (typeof containerElOrId === 'string') { + // cannot be empty + if (containerElOrId === '') { + window.alert('ChessBoard Error 1001: ' + + 'The first argument to ChessBoard() cannot be an empty string.' + + '\n\nExiting...'); + return false; + } + + // make sure the container element exists in the DOM + var el = document.getElementById(containerElOrId); + if (! el) { + window.alert('ChessBoard Error 1002: Element with id "' + + containerElOrId + '" does not exist in the DOM.' + + '\n\nExiting...'); + return false; + } + + // set the containerEl + containerEl = $(el); + } + + // else it must be something that becomes a jQuery collection + // with size 1 + // ie: a single DOM node or jQuery object + else { + containerEl = $(containerElOrId); + + if (containerEl.length !== 1) { + window.alert('ChessBoard Error 1003: The first argument to ' + + 'ChessBoard() must be an ID or a single DOM node.' + + '\n\nExiting...'); + return false; + } + } + + // JSON must exist + if (! window.JSON || + typeof JSON.stringify !== 'function' || + typeof JSON.parse !== 'function') { + window.alert('ChessBoard Error 1004: JSON does not exist. ' + + 'Please include a JSON polyfill.\n\nExiting...'); + return false; + } + + // check for a compatible version of jQuery + if (! (typeof window.$ && $.fn && $.fn.jquery && + compareSemVer($.fn.jquery, MINIMUM_JQUERY_VERSION) === true)) { + window.alert('ChessBoard Error 1005: Unable to find a valid version ' + + 'of jQuery. Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or ' + + 'higher on the page.\n\nExiting...'); + return false; + } + + return true; +} + +function validAnimationSpeed(speed) { + if (speed === 'fast' || speed === 'slow') { + return true; + } + + if ((parseInt(speed, 10) + '') !== (speed + '')) { + return false; + } + + return (speed >= 0); +} + +// validate config / set default options +function expandConfig() { + if (typeof cfg === 'string' || validPositionObject(cfg) === true) { + cfg = { + position: cfg + }; + } + + // default for orientation is white + if (cfg.orientation !== 'black') { + cfg.orientation = 'white'; + } + CURRENT_ORIENTATION = cfg.orientation; + + // default for showNotation is true + if (cfg.showNotation !== false) { + cfg.showNotation = true; + } + + // default for draggable is false + if (cfg.draggable !== true) { + cfg.draggable = false; + } + + // default for dropOffBoard is 'snapback' + if (cfg.dropOffBoard !== 'trash') { + cfg.dropOffBoard = 'snapback'; + } + + // default for sparePieces is false + if (cfg.sparePieces !== true) { + cfg.sparePieces = false; + } + + // draggable must be true if sparePieces is enabled + if (cfg.sparePieces === true) { + cfg.draggable = true; + } + + // default piece theme is wikipedia + if (cfg.hasOwnProperty('pieceTheme') !== true || + (typeof cfg.pieceTheme !== 'string' && + typeof cfg.pieceTheme !== 'function')) { + cfg.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png'; + } + + // animation speeds + if (cfg.hasOwnProperty('appearSpeed') !== true || + validAnimationSpeed(cfg.appearSpeed) !== true) { + cfg.appearSpeed = 200; + } + if (cfg.hasOwnProperty('moveSpeed') !== true || + validAnimationSpeed(cfg.moveSpeed) !== true) { + cfg.moveSpeed = 200; + } + if (cfg.hasOwnProperty('snapbackSpeed') !== true || + validAnimationSpeed(cfg.snapbackSpeed) !== true) { + cfg.snapbackSpeed = 50; + } + if (cfg.hasOwnProperty('snapSpeed') !== true || + validAnimationSpeed(cfg.snapSpeed) !== true) { + cfg.snapSpeed = 25; + } + if (cfg.hasOwnProperty('trashSpeed') !== true || + validAnimationSpeed(cfg.trashSpeed) !== true) { + cfg.trashSpeed = 100; + } + + // make sure position is valid + if (cfg.hasOwnProperty('position') === true) { + if (cfg.position === 'start') { + CURRENT_POSITION = deepCopy(START_POSITION); + } + + else if (validFen(cfg.position) === true) { + CURRENT_POSITION = fenToObj(cfg.position); + } + + else if (validPositionObject(cfg.position) === true) { + CURRENT_POSITION = deepCopy(cfg.position); + } + + else { + error(7263, 'Invalid value passed to config.position.', cfg.position); + } + } + + return true; +} + +//------------------------------------------------------------------------------ +// DOM Misc +//------------------------------------------------------------------------------ + +// calculates square size based on the width of the container +// got a little CSS black magic here, so let me explain: +// get the width of the container element (could be anything), reduce by 1 for +// fudge factor, and then keep reducing until we find an exact mod 8 for +// our square size +function calculateSquareSize() { + var containerWidth = parseInt(containerEl.css('width'), 10); + + // defensive, prevent infinite loop + if (! containerWidth || containerWidth <= 0) { + return 0; + } + + // pad one pixel + var boardWidth = containerWidth - 1; + + while (boardWidth % 8 !== 0 && boardWidth > 0) { + boardWidth--; + } + + return (boardWidth / 8); +} + +// create random IDs for elements +function createElIds() { + // squares on the board + for (var i = 0; i < COLUMNS.length; i++) { + for (var j = 1; j <= 8; j++) { + var square = COLUMNS[i] + j; + SQUARE_ELS_IDS[square] = square + '-' + createId(); + } + } + + // spare pieces + var pieces = 'KQRBNP'.split(''); + for (var i = 0; i < pieces.length; i++) { + var whitePiece = 'w' + pieces[i]; + var blackPiece = 'b' + pieces[i]; + SPARE_PIECE_ELS_IDS[whitePiece] = whitePiece + '-' + createId(); + SPARE_PIECE_ELS_IDS[blackPiece] = blackPiece + '-' + createId(); + } +} + +//------------------------------------------------------------------------------ +// Markup Building +//------------------------------------------------------------------------------ + +function buildBoardContainer() { + var html = '
'; + + if (cfg.sparePieces === true) { + html += '
'; + } + + html += '
'; + + if (cfg.sparePieces === true) { + html += '
'; + } + + html += '
'; + + return html; +} + +/* +var buildSquare = function(color, size, id) { + var html = '
'; + + if (cfg.showNotation === true) { + + } + + html += '
'; + + return html; +}; +*/ + +function buildBoard(orientation) { + if (orientation !== 'black') { + orientation = 'white'; + } + + var html = ''; + + // algebraic notation / orientation + var alpha = deepCopy(COLUMNS); + var row = 8; + if (orientation === 'black') { + alpha.reverse(); + row = 1; + } + + var squareColor = 'white'; + for (var i = 0; i < 8; i++) { + html += '
'; + for (var j = 0; j < 8; j++) { + var square = alpha[j] + row; + + html += '
'; + + if (cfg.showNotation === true) { + // alpha notation + if ((orientation === 'white' && row === 1) || + (orientation === 'black' && row === 8)) { + html += '
' + + alpha[j] + '
'; + } + + // numeric notation + if (j === 0) { + html += '
' + + row + '
'; + } + } + + html += '
'; // end .square + + squareColor = (squareColor === 'white' ? 'black' : 'white'); + } + html += '
'; + + squareColor = (squareColor === 'white' ? 'black' : 'white'); + + if (orientation === 'white') { + row--; + } + else { + row++; + } + } + + return html; +} + +function buildPieceImgSrc(piece) { + if (typeof cfg.pieceTheme === 'function') { + return cfg.pieceTheme(piece); + } + + if (typeof cfg.pieceTheme === 'string') { + return cfg.pieceTheme.replace(/{piece}/g, piece); + } + + // NOTE: this should never happen + error(8272, 'Unable to build image source for cfg.pieceTheme.'); + return ''; +} + +function buildPiece(piece, hidden, id) { + var html = ''; + + return html; +} + +function buildSparePieces(color) { + var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP']; + if (color === 'black') { + pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP']; + } + + var html = ''; + for (var i = 0; i < pieces.length; i++) { + html += buildPiece(pieces[i], false, SPARE_PIECE_ELS_IDS[pieces[i]]); + } + + return html; +} + +//------------------------------------------------------------------------------ +// Animations +//------------------------------------------------------------------------------ + +function animateSquareToSquare(src, dest, piece, completeFn) { + // get information about the source and destination squares + var srcSquareEl = $('#' + SQUARE_ELS_IDS[src]); + var srcSquarePosition = srcSquareEl.offset(); + var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]); + var destSquarePosition = destSquareEl.offset(); + + // create the animated piece and absolutely position it + // over the source square + var animatedPieceId = createId(); + $('body').append(buildPiece(piece, true, animatedPieceId)); + var animatedPieceEl = $('#' + animatedPieceId); + animatedPieceEl.css({ + display: '', + position: 'absolute', + top: srcSquarePosition.top, + left: srcSquarePosition.left + }); + + // remove original piece from source square + srcSquareEl.find('.' + CSS.piece).remove(); + + // on complete + var complete = function() { + // add the "real" piece to the destination square + destSquareEl.append(buildPiece(piece)); + + // remove the animated piece + animatedPieceEl.remove(); + + // run complete function + if (typeof completeFn === 'function') { + completeFn(); + } + }; + + // animate the piece to the destination square + var opts = { + duration: cfg.moveSpeed, + complete: complete + }; + animatedPieceEl.animate(destSquarePosition, opts); +} + +function animateSparePieceToSquare(piece, dest, completeFn) { + var srcOffset = $('#' + SPARE_PIECE_ELS_IDS[piece]).offset(); + var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]); + var destOffset = destSquareEl.offset(); + + // create the animate piece + var pieceId = createId(); + $('body').append(buildPiece(piece, true, pieceId)); + var animatedPieceEl = $('#' + pieceId); + animatedPieceEl.css({ + display: '', + position: 'absolute', + left: srcOffset.left, + top: srcOffset.top + }); + + // on complete + var complete = function() { + // add the "real" piece to the destination square + destSquareEl.find('.' + CSS.piece).remove(); + destSquareEl.append(buildPiece(piece)); + + // remove the animated piece + animatedPieceEl.remove(); + + // run complete function + if (typeof completeFn === 'function') { + completeFn(); + } + }; + + // animate the piece to the destination square + var opts = { + duration: cfg.moveSpeed, + complete: complete + }; + animatedPieceEl.animate(destOffset, opts); +} + +// execute an array of animations +function doAnimations(a, oldPos, newPos) { + ANIMATION_HAPPENING = true; + + var numFinished = 0; + function onFinish() { + numFinished++; + + // exit if all the animations aren't finished + if (numFinished !== a.length) return; + + drawPositionInstant(); + ANIMATION_HAPPENING = false; + + // run their onMoveEnd function + if (cfg.hasOwnProperty('onMoveEnd') === true && + typeof cfg.onMoveEnd === 'function') { + cfg.onMoveEnd(deepCopy(oldPos), deepCopy(newPos)); + } + } + + for (var i = 0; i < a.length; i++) { + // clear a piece + if (a[i].type === 'clear') { + $('#' + SQUARE_ELS_IDS[a[i].square] + ' .' + CSS.piece) + .fadeOut(cfg.trashSpeed, onFinish); + } + + // add a piece (no spare pieces) + if (a[i].type === 'add' && cfg.sparePieces !== true) { + $('#' + SQUARE_ELS_IDS[a[i].square]) + .append(buildPiece(a[i].piece, true)) + .find('.' + CSS.piece) + .fadeIn(cfg.appearSpeed, onFinish); + } + + // add a piece from a spare piece + if (a[i].type === 'add' && cfg.sparePieces === true) { + animateSparePieceToSquare(a[i].piece, a[i].square, onFinish); + } + + // move a piece + if (a[i].type === 'move') { + animateSquareToSquare(a[i].source, a[i].destination, a[i].piece, + onFinish); + } + } +} + +// returns the distance between two squares +function squareDistance(s1, s2) { + s1 = s1.split(''); + var s1x = COLUMNS.indexOf(s1[0]) + 1; + var s1y = parseInt(s1[1], 10); + + s2 = s2.split(''); + var s2x = COLUMNS.indexOf(s2[0]) + 1; + var s2y = parseInt(s2[1], 10); + + var xDelta = Math.abs(s1x - s2x); + var yDelta = Math.abs(s1y - s2y); + + if (xDelta >= yDelta) return xDelta; + return yDelta; +} + +// returns an array of closest squares from square +function createRadius(square) { + var squares = []; + + // calculate distance of all squares + for (var i = 0; i < 8; i++) { + for (var j = 0; j < 8; j++) { + var s = COLUMNS[i] + (j + 1); + + // skip the square we're starting from + if (square === s) continue; + + squares.push({ + square: s, + distance: squareDistance(square, s) + }); + } + } + + // sort by distance + squares.sort(function(a, b) { + return a.distance - b.distance; + }); + + // just return the square code + var squares2 = []; + for (var i = 0; i < squares.length; i++) { + squares2.push(squares[i].square); + } + + return squares2; +} + +// returns the square of the closest instance of piece +// returns false if no instance of piece is found in position +function findClosestPiece(position, piece, square) { + // create array of closest squares from square + var closestSquares = createRadius(square); + + // search through the position in order of distance for the piece + for (var i = 0; i < closestSquares.length; i++) { + var s = closestSquares[i]; + + if (position.hasOwnProperty(s) === true && position[s] === piece) { + return s; + } + } + + return false; +} + +// calculate an array of animations that need to happen in order to get +// from pos1 to pos2 +function calculateAnimations(pos1, pos2) { + // make copies of both + pos1 = deepCopy(pos1); + pos2 = deepCopy(pos2); + + var animations = []; + var squaresMovedTo = {}; + + // remove pieces that are the same in both positions + for (var i in pos2) { + if (pos2.hasOwnProperty(i) !== true) continue; + + if (pos1.hasOwnProperty(i) === true && pos1[i] === pos2[i]) { + delete pos1[i]; + delete pos2[i]; + } + } + + // find all the "move" animations + for (var i in pos2) { + if (pos2.hasOwnProperty(i) !== true) continue; + + var closestPiece = findClosestPiece(pos1, pos2[i], i); + if (closestPiece !== false) { + animations.push({ + type: 'move', + source: closestPiece, + destination: i, + piece: pos2[i] + }); + + delete pos1[closestPiece]; + delete pos2[i]; + squaresMovedTo[i] = true; + } + } + + // add pieces to pos2 + for (var i in pos2) { + if (pos2.hasOwnProperty(i) !== true) continue; + + animations.push({ + type: 'add', + square: i, + piece: pos2[i] + }) + + delete pos2[i]; + } + + // clear pieces from pos1 + for (var i in pos1) { + if (pos1.hasOwnProperty(i) !== true) continue; + + // do not clear a piece if it is on a square that is the result + // of a "move", ie: a piece capture + if (squaresMovedTo.hasOwnProperty(i) === true) continue; + + animations.push({ + type: 'clear', + square: i, + piece: pos1[i] + }); + + delete pos1[i]; + } + + return animations; +} + +//------------------------------------------------------------------------------ +// Control Flow +//------------------------------------------------------------------------------ + +function drawPositionInstant() { + // clear the board + boardEl.find('.' + CSS.piece).remove(); + + // add the pieces + for (var i in CURRENT_POSITION) { + if (CURRENT_POSITION.hasOwnProperty(i) !== true) continue; + + $('#' + SQUARE_ELS_IDS[i]).append(buildPiece(CURRENT_POSITION[i])); + } +} + +function drawBoard() { + boardEl.html(buildBoard(CURRENT_ORIENTATION)); + drawPositionInstant(); + + if (cfg.sparePieces === true) { + if (CURRENT_ORIENTATION === 'white') { + sparePiecesTopEl.html(buildSparePieces('black')); + sparePiecesBottomEl.html(buildSparePieces('white')); + } + else { + sparePiecesTopEl.html(buildSparePieces('white')); + sparePiecesBottomEl.html(buildSparePieces('black')); + } + } +} + +// given a position and a set of moves, return a new position +// with the moves executed +function calculatePositionFromMoves(position, moves) { + position = deepCopy(position); + + for (var i in moves) { + if (moves.hasOwnProperty(i) !== true) continue; + + // skip the move if the position doesn't have a piece on the source square + if (position.hasOwnProperty(i) !== true) continue; + + var piece = position[i]; + delete position[i]; + position[moves[i]] = piece; + } + + return position; +} + +function setCurrentPosition(position) { + var oldPos = deepCopy(CURRENT_POSITION); + var newPos = deepCopy(position); + var oldFen = objToFen(oldPos); + var newFen = objToFen(newPos); + + // do nothing if no change in position + if (oldFen === newFen) return; + + // run their onChange function + if (cfg.hasOwnProperty('onChange') === true && + typeof cfg.onChange === 'function') { + cfg.onChange(oldPos, newPos); + } + + // update state + CURRENT_POSITION = position; +} + +function isXYOnSquare(x, y) { + for (var i in SQUARE_ELS_OFFSETS) { + if (SQUARE_ELS_OFFSETS.hasOwnProperty(i) !== true) continue; + + var s = SQUARE_ELS_OFFSETS[i]; + if (x >= s.left && x < s.left + SQUARE_SIZE && + y >= s.top && y < s.top + SQUARE_SIZE) { + return i; + } + } + + return 'offboard'; +} + +// records the XY coords of every square into memory +function captureSquareOffsets() { + SQUARE_ELS_OFFSETS = {}; + + for (var i in SQUARE_ELS_IDS) { + if (SQUARE_ELS_IDS.hasOwnProperty(i) !== true) continue; + + SQUARE_ELS_OFFSETS[i] = $('#' + SQUARE_ELS_IDS[i]).offset(); + } +} + +function removeSquareHighlights() { + boardEl.find('.' + CSS.square) + .removeClass(CSS.highlight1 + ' ' + CSS.highlight2); +} + +function snapbackDraggedPiece() { + // there is no "snapback" for spare pieces + if (DRAGGED_PIECE_SOURCE === 'spare') { + trashDraggedPiece(); + return; + } + + removeSquareHighlights(); + + // animation complete + function complete() { + drawPositionInstant(); + draggedPieceEl.css('display', 'none'); + + // run their onSnapbackEnd function + if (cfg.hasOwnProperty('onSnapbackEnd') === true && + typeof cfg.onSnapbackEnd === 'function') { + cfg.onSnapbackEnd(DRAGGED_PIECE, DRAGGED_PIECE_SOURCE, + deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION); + } + } + + // get source square position + var sourceSquarePosition = + $('#' + SQUARE_ELS_IDS[DRAGGED_PIECE_SOURCE]).offset(); + + // animate the piece to the target square + var opts = { + duration: cfg.snapbackSpeed, + complete: complete + }; + draggedPieceEl.animate(sourceSquarePosition, opts); + + // set state + DRAGGING_A_PIECE = false; +} + +function trashDraggedPiece() { + removeSquareHighlights(); + + // remove the source piece + var newPosition = deepCopy(CURRENT_POSITION); + delete newPosition[DRAGGED_PIECE_SOURCE]; + setCurrentPosition(newPosition); + + // redraw the position + drawPositionInstant(); + + // hide the dragged piece + draggedPieceEl.fadeOut(cfg.trashSpeed); + + // set state + DRAGGING_A_PIECE = false; +} + +function dropDraggedPieceOnSquare(square) { + removeSquareHighlights(); + + // update position + var newPosition = deepCopy(CURRENT_POSITION); + delete newPosition[DRAGGED_PIECE_SOURCE]; + newPosition[square] = DRAGGED_PIECE; + setCurrentPosition(newPosition); + + // get target square information + var targetSquarePosition = $('#' + SQUARE_ELS_IDS[square]).offset(); + + // animation complete + var complete = function() { + drawPositionInstant(); + draggedPieceEl.css('display', 'none'); + + // execute their onSnapEnd function + if (cfg.hasOwnProperty('onSnapEnd') === true && + typeof cfg.onSnapEnd === 'function') { + cfg.onSnapEnd(DRAGGED_PIECE_SOURCE, square, DRAGGED_PIECE); + } + }; + + // snap the piece to the target square + var opts = { + duration: cfg.snapSpeed, + complete: complete + }; + draggedPieceEl.animate(targetSquarePosition, opts); + + // set state + DRAGGING_A_PIECE = false; +} + +function beginDraggingPiece(source, piece, x, y) { + // run their custom onDragStart function + // their custom onDragStart function can cancel drag start + if (typeof cfg.onDragStart === 'function' && + cfg.onDragStart(source, piece, + deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION) === false) { + return; + } + + // set state + DRAGGING_A_PIECE = true; + DRAGGED_PIECE = piece; + DRAGGED_PIECE_SOURCE = source; + + // if the piece came from spare pieces, location is offboard + if (source === 'spare') { + DRAGGED_PIECE_LOCATION = 'offboard'; + } + else { + DRAGGED_PIECE_LOCATION = source; + } + + // capture the x, y coords of all squares in memory + captureSquareOffsets(); + + // create the dragged piece + draggedPieceEl.attr('src', buildPieceImgSrc(piece)) + .css({ + display: '', + position: 'absolute', + left: x - (SQUARE_SIZE / 2), + top: y - (SQUARE_SIZE / 2) + }); + + if (source !== 'spare') { + // highlight the source square and hide the piece + $('#' + SQUARE_ELS_IDS[source]).addClass(CSS.highlight1) + .find('.' + CSS.piece).css('display', 'none'); + } +} + +function updateDraggedPiece(x, y) { + // put the dragged piece over the mouse cursor + draggedPieceEl.css({ + left: x - (SQUARE_SIZE / 2), + top: y - (SQUARE_SIZE / 2) + }); + + // get location + var location = isXYOnSquare(x, y); + + // do nothing if the location has not changed + if (location === DRAGGED_PIECE_LOCATION) return; + + // remove highlight from previous square + if (validSquare(DRAGGED_PIECE_LOCATION) === true) { + $('#' + SQUARE_ELS_IDS[DRAGGED_PIECE_LOCATION]) + .removeClass(CSS.highlight2); + } + + // add highlight to new square + if (validSquare(location) === true) { + $('#' + SQUARE_ELS_IDS[location]).addClass(CSS.highlight2); + } + + // run onDragMove + if (typeof cfg.onDragMove === 'function') { + cfg.onDragMove(location, DRAGGED_PIECE_LOCATION, + DRAGGED_PIECE_SOURCE, DRAGGED_PIECE, + deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION); + } + + // update state + DRAGGED_PIECE_LOCATION = location; +} + +function stopDraggedPiece(location) { + // determine what the action should be + var action = 'drop'; + if (location === 'offboard' && cfg.dropOffBoard === 'snapback') { + action = 'snapback'; + } + if (location === 'offboard' && cfg.dropOffBoard === 'trash') { + action = 'trash'; + } + + // run their onDrop function, which can potentially change the drop action + if (cfg.hasOwnProperty('onDrop') === true && + typeof cfg.onDrop === 'function') { + var newPosition = deepCopy(CURRENT_POSITION); + + // source piece is a spare piece and position is off the board + //if (DRAGGED_PIECE_SOURCE === 'spare' && location === 'offboard') {...} + // position has not changed; do nothing + + // source piece is a spare piece and position is on the board + if (DRAGGED_PIECE_SOURCE === 'spare' && validSquare(location) === true) { + // add the piece to the board + newPosition[location] = DRAGGED_PIECE; + } + + // source piece was on the board and position is off the board + if (validSquare(DRAGGED_PIECE_SOURCE) === true && location === 'offboard') { + // remove the piece from the board + delete newPosition[DRAGGED_PIECE_SOURCE]; + } + + // source piece was on the board and position is on the board + if (validSquare(DRAGGED_PIECE_SOURCE) === true && + validSquare(location) === true) { + // move the piece + delete newPosition[DRAGGED_PIECE_SOURCE]; + newPosition[location] = DRAGGED_PIECE; + } + + var oldPosition = deepCopy(CURRENT_POSITION); + + var result = cfg.onDrop(DRAGGED_PIECE_SOURCE, location, DRAGGED_PIECE, + newPosition, oldPosition, CURRENT_ORIENTATION); + if (result === 'snapback' || result === 'trash') { + action = result; + } + } + + // do it! + if (action === 'snapback') { + snapbackDraggedPiece(); + } + else if (action === 'trash') { + trashDraggedPiece(); + } + else if (action === 'drop') { + dropDraggedPieceOnSquare(location); + } +} + +//------------------------------------------------------------------------------ +// Public Methods +//------------------------------------------------------------------------------ + +// clear the board +widget.clear = function(useAnimation) { + widget.position({}, useAnimation); +}; + +/* +// get or set config properties +// TODO: write this, GitHub Issue #1 +widget.config = function(arg1, arg2) { + // get the current config + if (arguments.length === 0) { + return deepCopy(cfg); + } +}; +*/ + +// remove the widget from the page +widget.destroy = function() { + // remove markup + containerEl.html(''); + draggedPieceEl.remove(); + + // remove event handlers + containerEl.unbind(); +}; + +// shorthand method to get the current FEN +widget.fen = function() { + return widget.position('fen'); +}; + +// flip orientation +widget.flip = function() { + widget.orientation('flip'); +}; + +/* +// TODO: write this, GitHub Issue #5 +widget.highlight = function() { + +}; +*/ + +// move pieces +widget.move = function() { + // no need to throw an error here; just do nothing + if (arguments.length === 0) return; + + var useAnimation = true; + + // collect the moves into an object + var moves = {}; + for (var i = 0; i < arguments.length; i++) { + // any "false" to this function means no animations + if (arguments[i] === false) { + useAnimation = false; + continue; + } + + // skip invalid arguments + if (validMove(arguments[i]) !== true) { + error(2826, 'Invalid move passed to the move method.', arguments[i]); + continue; + } + + var tmp = arguments[i].split('-'); + moves[tmp[0]] = tmp[1]; + } + + // calculate position from moves + var newPos = calculatePositionFromMoves(CURRENT_POSITION, moves); + + // update the board + widget.position(newPos, useAnimation); + + // return the new position object + return newPos; +}; + +widget.orientation = function(arg) { + // no arguments, return the current orientation + if (arguments.length === 0) { + return CURRENT_ORIENTATION; + } + + // set to white or black + if (arg === 'white' || arg === 'black') { + CURRENT_ORIENTATION = arg; + drawBoard(); + return; + } + + // flip orientation + if (arg === 'flip') { + CURRENT_ORIENTATION = (CURRENT_ORIENTATION === 'white') ? 'black' : 'white'; + drawBoard(); + return; + } + + error(5482, 'Invalid value passed to the orientation method.', arg); +}; + +widget.position = function(position, useAnimation) { + // no arguments, return the current position + if (arguments.length === 0) { + return deepCopy(CURRENT_POSITION); + } + + // get position as FEN + if (typeof position === 'string' && position.toLowerCase() === 'fen') { + return objToFen(CURRENT_POSITION); + } + + // default for useAnimations is true + if (useAnimation !== false) { + useAnimation = true; + } + + // start position + if (typeof position === 'string' && position.toLowerCase() === 'start') { + position = deepCopy(START_POSITION); + } + + // convert FEN to position object + if (validFen(position) === true) { + position = fenToObj(position); + } + + // validate position object + if (validPositionObject(position) !== true) { + error(6482, 'Invalid value passed to the position method.', position); + return; + } + + if (useAnimation === true) { + // start the animations + doAnimations(calculateAnimations(CURRENT_POSITION, position), + CURRENT_POSITION, position); + + // set the new position + setCurrentPosition(position); + } + // instant update + else { + setCurrentPosition(position); + drawPositionInstant(); + } +}; + +widget.resize = function() { + // calulate the new square size + SQUARE_SIZE = calculateSquareSize(); + + // set board width + boardEl.css('width', (SQUARE_SIZE * 8) + 'px'); + + // set drag piece size + draggedPieceEl.css({ + height: SQUARE_SIZE, + width: SQUARE_SIZE + }); + + // spare pieces + if (cfg.sparePieces === true) { + containerEl.find('.' + CSS.sparePieces) + .css('paddingLeft', (SQUARE_SIZE + BOARD_BORDER_SIZE) + 'px'); + } + + // redraw the board + drawBoard(); +}; + +// set the starting position +widget.start = function(useAnimation) { + widget.position('start', useAnimation); +}; + +//------------------------------------------------------------------------------ +// Browser Events +//------------------------------------------------------------------------------ + +function isTouchDevice() { + return ('ontouchstart' in document.documentElement); +} + +// reference: http://www.quirksmode.org/js/detect.html +function isMSIE() { + return (navigator && navigator.userAgent && + navigator.userAgent.search(/MSIE/) !== -1); +} + +function stopDefault(e) { + e.preventDefault(); +} + +function mousedownSquare(e) { + // do nothing if we're not draggable + if (cfg.draggable !== true) return; + + var square = $(this).attr('data-square'); + + // no piece on this square + if (validSquare(square) !== true || + CURRENT_POSITION.hasOwnProperty(square) !== true) { + return; + } + + beginDraggingPiece(square, CURRENT_POSITION[square], e.pageX, e.pageY); +} + +function touchstartSquare(e) { + // do nothing if we're not draggable + if (cfg.draggable !== true) return; + + var square = $(this).attr('data-square'); + + // no piece on this square + if (validSquare(square) !== true || + CURRENT_POSITION.hasOwnProperty(square) !== true) { + return; + } + + e = e.originalEvent; + beginDraggingPiece(square, CURRENT_POSITION[square], + e.changedTouches[0].pageX, e.changedTouches[0].pageY); +} + +function mousedownSparePiece(e) { + // do nothing if sparePieces is not enabled + if (cfg.sparePieces !== true) return; + + var piece = $(this).attr('data-piece'); + + beginDraggingPiece('spare', piece, e.pageX, e.pageY); +} + +function touchstartSparePiece(e) { + // do nothing if sparePieces is not enabled + if (cfg.sparePieces !== true) return; + + var piece = $(this).attr('data-piece'); + + e = e.originalEvent; + beginDraggingPiece('spare', piece, + e.changedTouches[0].pageX, e.changedTouches[0].pageY); +} + +function mousemoveWindow(e) { + // do nothing if we are not dragging a piece + if (DRAGGING_A_PIECE !== true) return; + + updateDraggedPiece(e.pageX, e.pageY); +} + +function touchmoveWindow(e) { + // do nothing if we are not dragging a piece + if (DRAGGING_A_PIECE !== true) return; + + // prevent screen from scrolling + e.preventDefault(); + + updateDraggedPiece(e.originalEvent.changedTouches[0].pageX, + e.originalEvent.changedTouches[0].pageY); +} + +function mouseupWindow(e) { + // do nothing if we are not dragging a piece + if (DRAGGING_A_PIECE !== true) return; + + // get the location + var location = isXYOnSquare(e.pageX, e.pageY); + + stopDraggedPiece(location); +} + +function touchendWindow(e) { + // do nothing if we are not dragging a piece + if (DRAGGING_A_PIECE !== true) return; + + // get the location + var location = isXYOnSquare(e.originalEvent.changedTouches[0].pageX, + e.originalEvent.changedTouches[0].pageY); + + stopDraggedPiece(location); +} + +function mouseenterSquare(e) { + // do not fire this event if we are dragging a piece + // NOTE: this should never happen, but it's a safeguard + if (DRAGGING_A_PIECE !== false) return; + + if (cfg.hasOwnProperty('onMouseoverSquare') !== true || + typeof cfg.onMouseoverSquare !== 'function') return; + + // get the square + var square = $(e.currentTarget).attr('data-square'); + + // NOTE: this should never happen; defensive + if (validSquare(square) !== true) return; + + // get the piece on this square + var piece = false; + if (CURRENT_POSITION.hasOwnProperty(square) === true) { + piece = CURRENT_POSITION[square]; + } + + // execute their function + cfg.onMouseoverSquare(square, piece, deepCopy(CURRENT_POSITION), + CURRENT_ORIENTATION); +} + +function mouseleaveSquare(e) { + // do not fire this event if we are dragging a piece + // NOTE: this should never happen, but it's a safeguard + if (DRAGGING_A_PIECE !== false) return; + + if (cfg.hasOwnProperty('onMouseoutSquare') !== true || + typeof cfg.onMouseoutSquare !== 'function') return; + + // get the square + var square = $(e.currentTarget).attr('data-square'); + + // NOTE: this should never happen; defensive + if (validSquare(square) !== true) return; + + // get the piece on this square + var piece = false; + if (CURRENT_POSITION.hasOwnProperty(square) === true) { + piece = CURRENT_POSITION[square]; + } + + // execute their function + cfg.onMouseoutSquare(square, piece, deepCopy(CURRENT_POSITION), + CURRENT_ORIENTATION); +} + +//------------------------------------------------------------------------------ +// Initialization +//------------------------------------------------------------------------------ + +function addEvents() { + // prevent browser "image drag" + $('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault); + + // mouse drag pieces + boardEl.on('mousedown', '.' + CSS.square, mousedownSquare); + containerEl.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, + mousedownSparePiece); + + // mouse enter / leave square + boardEl.on('mouseenter', '.' + CSS.square, mouseenterSquare); + boardEl.on('mouseleave', '.' + CSS.square, mouseleaveSquare); + + // IE doesn't like the events on the window object, but other browsers + // perform better that way + if (isMSIE() === true) { + // IE-specific prevent browser "image drag" + document.ondragstart = function() { return false; }; + + $('body').on('mousemove', mousemoveWindow); + $('body').on('mouseup', mouseupWindow); + } + else { + $(window).on('mousemove', mousemoveWindow); + $(window).on('mouseup', mouseupWindow); + } + + // touch drag pieces + if (isTouchDevice() === true) { + boardEl.on('touchstart', '.' + CSS.square, touchstartSquare); + containerEl.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, + touchstartSparePiece); + $(window).on('touchmove', touchmoveWindow); + $(window).on('touchend', touchendWindow); + } +} + +function initDom() { + // build board and save it in memory + containerEl.html(buildBoardContainer()); + boardEl = containerEl.find('.' + CSS.board); + + if (cfg.sparePieces === true) { + sparePiecesTopEl = containerEl.find('.' + CSS.sparePiecesTop); + sparePiecesBottomEl = containerEl.find('.' + CSS.sparePiecesBottom); + } + + // create the drag piece + var draggedPieceId = createId(); + $('body').append(buildPiece('wP', true, draggedPieceId)); + draggedPieceEl = $('#' + draggedPieceId); + + // get the border size + BOARD_BORDER_SIZE = parseInt(boardEl.css('borderLeftWidth'), 10); + + // set the size and draw the board + widget.resize(); +} + +function init() { + if (checkDeps() !== true || + expandConfig() !== true) return; + + // create unique IDs for all the elements we will create + createElIds(); + + initDom(); + addEvents(); +} + +// go time +init(); + +// return the widget object +return widget; + +}; // end window.ChessBoard + +// expose util functions +window.ChessBoard.fenToObj = fenToObj; +window.ChessBoard.objToFen = objToFen; + +})(); // end anonymous wrapper diff --git a/wB.png b/wB.png new file mode 100644 index 0000000000000000000000000000000000000000..70e0e14088f6ea09b84b728df6aaae6d4776ab4e GIT binary patch literal 2374 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 Z{12-kJ&=_`^N#=k002ovPDHLkV1l3JS(E?( literal 0 HcmV?d00001 diff --git a/wN.png b/wN.png new file mode 100644 index 0000000000000000000000000000000000000000..237250c164f652f0ea61f5e8f65c9b4cb83489b7 GIT binary patch literal 2388 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 V@B7wJo3#J{002ovPDHLkV1iHz`5piO literal 0 HcmV?d00001 diff --git a/wQ.png b/wQ.png new file mode 100644 index 0000000000000000000000000000000000000000..c3dfc15e556cbbeb546374aa9ee5e6bf121e929d GIT binary patch literal 3812 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