christopher commited on
Commit
1a29994
·
1 Parent(s): d32c74d

Upload 16 files

Browse files
static/chessboard-1.0.0.css ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*! chessboard.js v1.0.0 | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */
2
+
3
+ .clearfix-7da63 {
4
+ clear: both;
5
+ }
6
+
7
+ .board-b72b1 {
8
+ border: 2px solid #404040;
9
+ box-sizing: content-box;
10
+ }
11
+
12
+ .square-55d63 {
13
+ float: left;
14
+ position: relative;
15
+
16
+ /* disable any native browser highlighting */
17
+ -webkit-touch-callout: none;
18
+ -webkit-user-select: none;
19
+ -khtml-user-select: none;
20
+ -moz-user-select: none;
21
+ -ms-user-select: none;
22
+ user-select: none;
23
+ }
24
+
25
+ .white-1e1d7 {
26
+ background-color: #f0d9b5;
27
+ color: #b58863;
28
+ }
29
+
30
+ .black-3c85d {
31
+ background-color: #b58863;
32
+ color: #f0d9b5;
33
+ }
34
+
35
+ .highlight1-32417, .highlight2-9c5d2 {
36
+ box-shadow: inset 0 0 3px 3px yellow;
37
+ }
38
+
39
+ .notation-322f9 {
40
+ cursor: default;
41
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
42
+ font-size: 14px;
43
+ position: absolute;
44
+ }
45
+
46
+ .alpha-d2270 {
47
+ bottom: 1px;
48
+ right: 3px;
49
+ }
50
+
51
+ .numeric-fc462 {
52
+ top: 2px;
53
+ left: 2px;
54
+ }
static/chessboard-1.0.0.js ADDED
@@ -0,0 +1,1817 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // chessboard.js v1.0.0
2
+ // https://github.com/oakmac/chessboardjs/
3
+ //
4
+ // Copyright (c) 2019, Chris Oakman
5
+ // Released under the MIT license
6
+ // https://github.com/oakmac/chessboardjs/blob/master/LICENSE.md
7
+
8
+ // start anonymous scope
9
+ ;(function () {
10
+ 'use strict'
11
+
12
+ var $ = window['jQuery']
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+
18
+ var COLUMNS = 'abcdefgh'.split('')
19
+ var DEFAULT_DRAG_THROTTLE_RATE = 20
20
+ var ELLIPSIS = '…'
21
+ var MINIMUM_JQUERY_VERSION = '1.8.3'
22
+ var RUN_ASSERTS = false
23
+ var START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'
24
+ var START_POSITION = fenToObj(START_FEN)
25
+
26
+ // default animation speeds
27
+ var DEFAULT_APPEAR_SPEED = 200
28
+ var DEFAULT_MOVE_SPEED = 200
29
+ var DEFAULT_SNAPBACK_SPEED = 60
30
+ var DEFAULT_SNAP_SPEED = 30
31
+ var DEFAULT_TRASH_SPEED = 100
32
+
33
+ // use unique class names to prevent clashing with anything else on the page
34
+ // and simplify selectors
35
+ // NOTE: these should never change
36
+ var CSS = {}
37
+ CSS['alpha'] = 'alpha-d2270'
38
+ CSS['black'] = 'black-3c85d'
39
+ CSS['board'] = 'board-b72b1'
40
+ CSS['chessboard'] = 'chessboard-63f37'
41
+ CSS['clearfix'] = 'clearfix-7da63'
42
+ CSS['highlight1'] = 'highlight1-32417'
43
+ CSS['highlight2'] = 'highlight2-9c5d2'
44
+ CSS['notation'] = 'notation-322f9'
45
+ CSS['numeric'] = 'numeric-fc462'
46
+ CSS['piece'] = 'piece-417db'
47
+ CSS['row'] = 'row-5277c'
48
+ CSS['sparePieces'] = 'spare-pieces-7492f'
49
+ CSS['sparePiecesBottom'] = 'spare-pieces-bottom-ae20f'
50
+ CSS['sparePiecesTop'] = 'spare-pieces-top-4028b'
51
+ CSS['square'] = 'square-55d63'
52
+ CSS['white'] = 'white-1e1d7'
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Misc Util Functions
56
+ // ---------------------------------------------------------------------------
57
+
58
+ function throttle (f, interval, scope) {
59
+ var timeout = 0
60
+ var shouldFire = false
61
+ var args = []
62
+
63
+ var handleTimeout = function () {
64
+ timeout = 0
65
+ if (shouldFire) {
66
+ shouldFire = false
67
+ fire()
68
+ }
69
+ }
70
+
71
+ var fire = function () {
72
+ timeout = window.setTimeout(handleTimeout, interval)
73
+ f.apply(scope, args)
74
+ }
75
+
76
+ return function (_args) {
77
+ args = arguments
78
+ if (!timeout) {
79
+ fire()
80
+ } else {
81
+ shouldFire = true
82
+ }
83
+ }
84
+ }
85
+
86
+ // function debounce (f, interval, scope) {
87
+ // var timeout = 0
88
+ // return function (_args) {
89
+ // window.clearTimeout(timeout)
90
+ // var args = arguments
91
+ // timeout = window.setTimeout(function () {
92
+ // f.apply(scope, args)
93
+ // }, interval)
94
+ // }
95
+ // }
96
+
97
+ function uuid () {
98
+ return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function (c) {
99
+ var r = (Math.random() * 16) | 0
100
+ return r.toString(16)
101
+ })
102
+ }
103
+
104
+ function deepCopy (thing) {
105
+ return JSON.parse(JSON.stringify(thing))
106
+ }
107
+
108
+ function parseSemVer (version) {
109
+ var tmp = version.split('.')
110
+ return {
111
+ major: parseInt(tmp[0], 10),
112
+ minor: parseInt(tmp[1], 10),
113
+ patch: parseInt(tmp[2], 10)
114
+ }
115
+ }
116
+
117
+ // returns true if version is >= minimum
118
+ function validSemanticVersion (version, minimum) {
119
+ version = parseSemVer(version)
120
+ minimum = parseSemVer(minimum)
121
+
122
+ var versionNum = (version.major * 100000 * 100000) +
123
+ (version.minor * 100000) +
124
+ version.patch
125
+ var minimumNum = (minimum.major * 100000 * 100000) +
126
+ (minimum.minor * 100000) +
127
+ minimum.patch
128
+
129
+ return versionNum >= minimumNum
130
+ }
131
+
132
+ function interpolateTemplate (str, obj) {
133
+ for (var key in obj) {
134
+ if (!obj.hasOwnProperty(key)) continue
135
+ var keyTemplateStr = '{' + key + '}'
136
+ var value = obj[key]
137
+ while (str.indexOf(keyTemplateStr) !== -1) {
138
+ str = str.replace(keyTemplateStr, value)
139
+ }
140
+ }
141
+ return str
142
+ }
143
+
144
+ if (RUN_ASSERTS) {
145
+ console.assert(interpolateTemplate('abc', {a: 'x'}) === 'abc')
146
+ console.assert(interpolateTemplate('{a}bc', {}) === '{a}bc')
147
+ console.assert(interpolateTemplate('{a}bc', {p: 'q'}) === '{a}bc')
148
+ console.assert(interpolateTemplate('{a}bc', {a: 'x'}) === 'xbc')
149
+ console.assert(interpolateTemplate('{a}bc{a}bc', {a: 'x'}) === 'xbcxbc')
150
+ console.assert(interpolateTemplate('{a}{a}{b}', {a: 'x', b: 'y'}) === 'xxy')
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Predicates
155
+ // ---------------------------------------------------------------------------
156
+
157
+ function isString (s) {
158
+ return typeof s === 'string'
159
+ }
160
+
161
+ function isFunction (f) {
162
+ return typeof f === 'function'
163
+ }
164
+
165
+ function isInteger (n) {
166
+ return typeof n === 'number' &&
167
+ isFinite(n) &&
168
+ Math.floor(n) === n
169
+ }
170
+
171
+ function validAnimationSpeed (speed) {
172
+ if (speed === 'fast' || speed === 'slow') return true
173
+ if (!isInteger(speed)) return false
174
+ return speed >= 0
175
+ }
176
+
177
+ function validThrottleRate (rate) {
178
+ return isInteger(rate) &&
179
+ rate >= 1
180
+ }
181
+
182
+ function validMove (move) {
183
+ // move should be a string
184
+ if (!isString(move)) return false
185
+
186
+ // move should be in the form of "e2-e4", "f6-d5"
187
+ var squares = move.split('-')
188
+ if (squares.length !== 2) return false
189
+
190
+ return validSquare(squares[0]) && validSquare(squares[1])
191
+ }
192
+
193
+ function validSquare (square) {
194
+ return isString(square) && square.search(/^[a-h][1-8]$/) !== -1
195
+ }
196
+
197
+ if (RUN_ASSERTS) {
198
+ console.assert(validSquare('a1'))
199
+ console.assert(validSquare('e2'))
200
+ console.assert(!validSquare('D2'))
201
+ console.assert(!validSquare('g9'))
202
+ console.assert(!validSquare('a'))
203
+ console.assert(!validSquare(true))
204
+ console.assert(!validSquare(null))
205
+ console.assert(!validSquare({}))
206
+ }
207
+
208
+ function validPieceCode (code) {
209
+ return isString(code) && code.search(/^[bw][KQRNBP]$/) !== -1
210
+ }
211
+
212
+ if (RUN_ASSERTS) {
213
+ console.assert(validPieceCode('bP'))
214
+ console.assert(validPieceCode('bK'))
215
+ console.assert(validPieceCode('wK'))
216
+ console.assert(validPieceCode('wR'))
217
+ console.assert(!validPieceCode('WR'))
218
+ console.assert(!validPieceCode('Wr'))
219
+ console.assert(!validPieceCode('a'))
220
+ console.assert(!validPieceCode(true))
221
+ console.assert(!validPieceCode(null))
222
+ console.assert(!validPieceCode({}))
223
+ }
224
+
225
+ function validFen (fen) {
226
+ if (!isString(fen)) return false
227
+
228
+ // cut off any move, castling, etc info from the end
229
+ // we're only interested in position information
230
+ fen = fen.replace(/ .+$/, '')
231
+
232
+ // expand the empty square numbers to just 1s
233
+ fen = expandFenEmptySquares(fen)
234
+
235
+ // FEN should be 8 sections separated by slashes
236
+ var chunks = fen.split('/')
237
+ if (chunks.length !== 8) return false
238
+
239
+ // check each section
240
+ for (var i = 0; i < 8; i++) {
241
+ if (chunks[i].length !== 8 ||
242
+ chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) {
243
+ return false
244
+ }
245
+ }
246
+
247
+ return true
248
+ }
249
+
250
+ if (RUN_ASSERTS) {
251
+ console.assert(validFen(START_FEN))
252
+ console.assert(validFen('8/8/8/8/8/8/8/8'))
253
+ console.assert(validFen('r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R'))
254
+ console.assert(validFen('3r3r/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1'))
255
+ console.assert(!validFen('3r3z/1p4pp/2nb1k2/pP3p2/8/PB2PN2/p4PPP/R4RK1 b - - 0 1'))
256
+ console.assert(!validFen('anbqkbnr/8/8/8/8/8/PPPPPPPP/8'))
257
+ console.assert(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/'))
258
+ console.assert(!validFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBN'))
259
+ console.assert(!validFen('888888/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'))
260
+ console.assert(!validFen('888888/pppppppp/74/8/8/8/PPPPPPPP/RNBQKBNR'))
261
+ console.assert(!validFen({}))
262
+ }
263
+
264
+ function validPositionObject (pos) {
265
+ if (!$.isPlainObject(pos)) return false
266
+
267
+ for (var i in pos) {
268
+ if (!pos.hasOwnProperty(i)) continue
269
+
270
+ if (!validSquare(i) || !validPieceCode(pos[i])) {
271
+ return false
272
+ }
273
+ }
274
+
275
+ return true
276
+ }
277
+
278
+ if (RUN_ASSERTS) {
279
+ console.assert(validPositionObject(START_POSITION))
280
+ console.assert(validPositionObject({}))
281
+ console.assert(validPositionObject({e2: 'wP'}))
282
+ console.assert(validPositionObject({e2: 'wP', d2: 'wP'}))
283
+ console.assert(!validPositionObject({e2: 'BP'}))
284
+ console.assert(!validPositionObject({y2: 'wP'}))
285
+ console.assert(!validPositionObject(null))
286
+ console.assert(!validPositionObject('start'))
287
+ console.assert(!validPositionObject(START_FEN))
288
+ }
289
+
290
+ function isTouchDevice () {
291
+ return 'ontouchstart' in document.documentElement
292
+ }
293
+
294
+ function validJQueryVersion () {
295
+ return typeof window.$ &&
296
+ $.fn &&
297
+ $.fn.jquery &&
298
+ validSemanticVersion($.fn.jquery, MINIMUM_JQUERY_VERSION)
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Chess Util Functions
303
+ // ---------------------------------------------------------------------------
304
+
305
+ // convert FEN piece code to bP, wK, etc
306
+ function fenToPieceCode (piece) {
307
+ // black piece
308
+ if (piece.toLowerCase() === piece) {
309
+ return 'b' + piece.toUpperCase()
310
+ }
311
+
312
+ // white piece
313
+ return 'w' + piece.toUpperCase()
314
+ }
315
+
316
+ // convert bP, wK, etc code to FEN structure
317
+ function pieceCodeToFen (piece) {
318
+ var pieceCodeLetters = piece.split('')
319
+
320
+ // white piece
321
+ if (pieceCodeLetters[0] === 'w') {
322
+ return pieceCodeLetters[1].toUpperCase()
323
+ }
324
+
325
+ // black piece
326
+ return pieceCodeLetters[1].toLowerCase()
327
+ }
328
+
329
+ // convert FEN string to position object
330
+ // returns false if the FEN string is invalid
331
+ function fenToObj (fen) {
332
+ if (!validFen(fen)) return false
333
+
334
+ // cut off any move, castling, etc info from the end
335
+ // we're only interested in position information
336
+ fen = fen.replace(/ .+$/, '')
337
+
338
+ var rows = fen.split('/')
339
+ var position = {}
340
+
341
+ var currentRow = 8
342
+ for (var i = 0; i < 8; i++) {
343
+ var row = rows[i].split('')
344
+ var colIdx = 0
345
+
346
+ // loop through each character in the FEN section
347
+ for (var j = 0; j < row.length; j++) {
348
+ // number / empty squares
349
+ if (row[j].search(/[1-8]/) !== -1) {
350
+ var numEmptySquares = parseInt(row[j], 10)
351
+ colIdx = colIdx + numEmptySquares
352
+ } else {
353
+ // piece
354
+ var square = COLUMNS[colIdx] + currentRow
355
+ position[square] = fenToPieceCode(row[j])
356
+ colIdx = colIdx + 1
357
+ }
358
+ }
359
+
360
+ currentRow = currentRow - 1
361
+ }
362
+
363
+ return position
364
+ }
365
+
366
+ // position object to FEN string
367
+ // returns false if the obj is not a valid position object
368
+ function objToFen (obj) {
369
+ if (!validPositionObject(obj)) return false
370
+
371
+ var fen = ''
372
+
373
+ var currentRow = 8
374
+ for (var i = 0; i < 8; i++) {
375
+ for (var j = 0; j < 8; j++) {
376
+ var square = COLUMNS[j] + currentRow
377
+
378
+ // piece exists
379
+ if (obj.hasOwnProperty(square)) {
380
+ fen = fen + pieceCodeToFen(obj[square])
381
+ } else {
382
+ // empty space
383
+ fen = fen + '1'
384
+ }
385
+ }
386
+
387
+ if (i !== 7) {
388
+ fen = fen + '/'
389
+ }
390
+
391
+ currentRow = currentRow - 1
392
+ }
393
+
394
+ // squeeze the empty numbers together
395
+ fen = squeezeFenEmptySquares(fen)
396
+
397
+ return fen
398
+ }
399
+
400
+ if (RUN_ASSERTS) {
401
+ console.assert(objToFen(START_POSITION) === START_FEN)
402
+ console.assert(objToFen({}) === '8/8/8/8/8/8/8/8')
403
+ console.assert(objToFen({a2: 'wP', 'b2': 'bP'}) === '8/8/8/8/8/8/Pp6/8')
404
+ }
405
+
406
+ function squeezeFenEmptySquares (fen) {
407
+ return fen.replace(/11111111/g, '8')
408
+ .replace(/1111111/g, '7')
409
+ .replace(/111111/g, '6')
410
+ .replace(/11111/g, '5')
411
+ .replace(/1111/g, '4')
412
+ .replace(/111/g, '3')
413
+ .replace(/11/g, '2')
414
+ }
415
+
416
+ function expandFenEmptySquares (fen) {
417
+ return fen.replace(/8/g, '11111111')
418
+ .replace(/7/g, '1111111')
419
+ .replace(/6/g, '111111')
420
+ .replace(/5/g, '11111')
421
+ .replace(/4/g, '1111')
422
+ .replace(/3/g, '111')
423
+ .replace(/2/g, '11')
424
+ }
425
+
426
+ // returns the distance between two squares
427
+ function squareDistance (squareA, squareB) {
428
+ var squareAArray = squareA.split('')
429
+ var squareAx = COLUMNS.indexOf(squareAArray[0]) + 1
430
+ var squareAy = parseInt(squareAArray[1], 10)
431
+
432
+ var squareBArray = squareB.split('')
433
+ var squareBx = COLUMNS.indexOf(squareBArray[0]) + 1
434
+ var squareBy = parseInt(squareBArray[1], 10)
435
+
436
+ var xDelta = Math.abs(squareAx - squareBx)
437
+ var yDelta = Math.abs(squareAy - squareBy)
438
+
439
+ if (xDelta >= yDelta) return xDelta
440
+ return yDelta
441
+ }
442
+
443
+ // returns the square of the closest instance of piece
444
+ // returns false if no instance of piece is found in position
445
+ function findClosestPiece (position, piece, square) {
446
+ // create array of closest squares from square
447
+ var closestSquares = createRadius(square)
448
+
449
+ // search through the position in order of distance for the piece
450
+ for (var i = 0; i < closestSquares.length; i++) {
451
+ var s = closestSquares[i]
452
+
453
+ if (position.hasOwnProperty(s) && position[s] === piece) {
454
+ return s
455
+ }
456
+ }
457
+
458
+ return false
459
+ }
460
+
461
+ // returns an array of closest squares from square
462
+ function createRadius (square) {
463
+ var squares = []
464
+
465
+ // calculate distance of all squares
466
+ for (var i = 0; i < 8; i++) {
467
+ for (var j = 0; j < 8; j++) {
468
+ var s = COLUMNS[i] + (j + 1)
469
+
470
+ // skip the square we're starting from
471
+ if (square === s) continue
472
+
473
+ squares.push({
474
+ square: s,
475
+ distance: squareDistance(square, s)
476
+ })
477
+ }
478
+ }
479
+
480
+ // sort by distance
481
+ squares.sort(function (a, b) {
482
+ return a.distance - b.distance
483
+ })
484
+
485
+ // just return the square code
486
+ var surroundingSquares = []
487
+ for (i = 0; i < squares.length; i++) {
488
+ surroundingSquares.push(squares[i].square)
489
+ }
490
+
491
+ return surroundingSquares
492
+ }
493
+
494
+ // given a position and a set of moves, return a new position
495
+ // with the moves executed
496
+ function calculatePositionFromMoves (position, moves) {
497
+ var newPosition = deepCopy(position)
498
+
499
+ for (var i in moves) {
500
+ if (!moves.hasOwnProperty(i)) continue
501
+
502
+ // skip the move if the position doesn't have a piece on the source square
503
+ if (!newPosition.hasOwnProperty(i)) continue
504
+
505
+ var piece = newPosition[i]
506
+ delete newPosition[i]
507
+ newPosition[moves[i]] = piece
508
+ }
509
+
510
+ return newPosition
511
+ }
512
+
513
+ // TODO: add some asserts here for calculatePositionFromMoves
514
+
515
+ // ---------------------------------------------------------------------------
516
+ // HTML
517
+ // ---------------------------------------------------------------------------
518
+
519
+ function buildContainerHTML (hasSparePieces) {
520
+ var html = '<div class="{chessboard}">'
521
+
522
+ if (hasSparePieces) {
523
+ html += '<div class="{sparePieces} {sparePiecesTop}"></div>'
524
+ }
525
+
526
+ html += '<div class="{board}"></div>'
527
+
528
+ if (hasSparePieces) {
529
+ html += '<div class="{sparePieces} {sparePiecesBottom}"></div>'
530
+ }
531
+
532
+ html += '</div>'
533
+
534
+ return interpolateTemplate(html, CSS)
535
+ }
536
+
537
+ // ---------------------------------------------------------------------------
538
+ // Config
539
+ // ---------------------------------------------------------------------------
540
+
541
+ function expandConfigArgumentShorthand (config) {
542
+ if (config === 'start') {
543
+ config = {position: deepCopy(START_POSITION)}
544
+ } else if (validFen(config)) {
545
+ config = {position: fenToObj(config)}
546
+ } else if (validPositionObject(config)) {
547
+ config = {position: deepCopy(config)}
548
+ }
549
+
550
+ // config must be an object
551
+ if (!$.isPlainObject(config)) config = {}
552
+
553
+ return config
554
+ }
555
+
556
+ // validate config / set default options
557
+ function expandConfig (config) {
558
+ // default for orientation is white
559
+ if (config.orientation !== 'black') config.orientation = 'white'
560
+
561
+ // default for showNotation is true
562
+ if (config.showNotation !== false) config.showNotation = true
563
+
564
+ // default for draggable is false
565
+ if (config.draggable !== true) config.draggable = false
566
+
567
+ // default for dropOffBoard is 'snapback'
568
+ if (config.dropOffBoard !== 'trash') config.dropOffBoard = 'snapback'
569
+
570
+ // default for sparePieces is false
571
+ if (config.sparePieces !== true) config.sparePieces = false
572
+
573
+ // draggable must be true if sparePieces is enabled
574
+ if (config.sparePieces) config.draggable = true
575
+
576
+ // default piece theme is wikipedia
577
+ if (!config.hasOwnProperty('pieceTheme') ||
578
+ (!isString(config.pieceTheme) && !isFunction(config.pieceTheme))) {
579
+ config.pieceTheme = 'img/chesspieces/wikipedia/{piece}.png'
580
+ }
581
+
582
+ // animation speeds
583
+ if (!validAnimationSpeed(config.appearSpeed)) config.appearSpeed = DEFAULT_APPEAR_SPEED
584
+ if (!validAnimationSpeed(config.moveSpeed)) config.moveSpeed = DEFAULT_MOVE_SPEED
585
+ if (!validAnimationSpeed(config.snapbackSpeed)) config.snapbackSpeed = DEFAULT_SNAPBACK_SPEED
586
+ if (!validAnimationSpeed(config.snapSpeed)) config.snapSpeed = DEFAULT_SNAP_SPEED
587
+ if (!validAnimationSpeed(config.trashSpeed)) config.trashSpeed = DEFAULT_TRASH_SPEED
588
+
589
+ // throttle rate
590
+ if (!validThrottleRate(config.dragThrottleRate)) config.dragThrottleRate = DEFAULT_DRAG_THROTTLE_RATE
591
+
592
+ return config
593
+ }
594
+
595
+ // ---------------------------------------------------------------------------
596
+ // Dependencies
597
+ // ---------------------------------------------------------------------------
598
+
599
+ // check for a compatible version of jQuery
600
+ function checkJQuery () {
601
+ if (!validJQueryVersion()) {
602
+ var errorMsg = 'Chessboard Error 1005: Unable to find a valid version of jQuery. ' +
603
+ 'Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or higher on the page' +
604
+ '\n\n' +
605
+ 'Exiting' + ELLIPSIS
606
+ window.alert(errorMsg)
607
+ return false
608
+ }
609
+
610
+ return true
611
+ }
612
+
613
+ // return either boolean false or the $container element
614
+ function checkContainerArg (containerElOrString) {
615
+ if (containerElOrString === '') {
616
+ var errorMsg1 = 'Chessboard Error 1001: ' +
617
+ 'The first argument to Chessboard() cannot be an empty string.' +
618
+ '\n\n' +
619
+ 'Exiting' + ELLIPSIS
620
+ window.alert(errorMsg1)
621
+ return false
622
+ }
623
+
624
+ // convert containerEl to query selector if it is a string
625
+ if (isString(containerElOrString) &&
626
+ containerElOrString.charAt(0) !== '#') {
627
+ containerElOrString = '#' + containerElOrString
628
+ }
629
+
630
+ // containerEl must be something that becomes a jQuery collection of size 1
631
+ var $container = $(containerElOrString)
632
+ if ($container.length !== 1) {
633
+ var errorMsg2 = 'Chessboard Error 1003: ' +
634
+ 'The first argument to Chessboard() must be the ID of a DOM node, ' +
635
+ 'an ID query selector, or a single DOM node.' +
636
+ '\n\n' +
637
+ 'Exiting' + ELLIPSIS
638
+ window.alert(errorMsg2)
639
+ return false
640
+ }
641
+
642
+ return $container
643
+ }
644
+
645
+ // ---------------------------------------------------------------------------
646
+ // Constructor
647
+ // ---------------------------------------------------------------------------
648
+
649
+ function constructor (containerElOrString, config) {
650
+ // first things first: check basic dependencies
651
+ if (!checkJQuery()) return null
652
+ var $container = checkContainerArg(containerElOrString)
653
+ if (!$container) return null
654
+
655
+ // ensure the config object is what we expect
656
+ config = expandConfigArgumentShorthand(config)
657
+ config = expandConfig(config)
658
+
659
+ // DOM elements
660
+ var $board = null
661
+ var $draggedPiece = null
662
+ var $sparePiecesTop = null
663
+ var $sparePiecesBottom = null
664
+
665
+ // constructor return object
666
+ var widget = {}
667
+
668
+ // -------------------------------------------------------------------------
669
+ // Stateful
670
+ // -------------------------------------------------------------------------
671
+
672
+ var boardBorderSize = 2
673
+ var currentOrientation = 'white'
674
+ var currentPosition = {}
675
+ var draggedPiece = null
676
+ var draggedPieceLocation = null
677
+ var draggedPieceSource = null
678
+ var isDragging = false
679
+ var sparePiecesElsIds = {}
680
+ var squareElsIds = {}
681
+ var squareElsOffsets = {}
682
+ var squareSize = 16
683
+
684
+ // -------------------------------------------------------------------------
685
+ // Validation / Errors
686
+ // -------------------------------------------------------------------------
687
+
688
+ function error (code, msg, obj) {
689
+ // do nothing if showErrors is not set
690
+ if (
691
+ config.hasOwnProperty('showErrors') !== true ||
692
+ config.showErrors === false
693
+ ) {
694
+ return
695
+ }
696
+
697
+ var errorText = 'Chessboard Error ' + code + ': ' + msg
698
+
699
+ // print to console
700
+ if (
701
+ config.showErrors === 'console' &&
702
+ typeof console === 'object' &&
703
+ typeof console.log === 'function'
704
+ ) {
705
+ console.log(errorText)
706
+ if (arguments.length >= 2) {
707
+ console.log(obj)
708
+ }
709
+ return
710
+ }
711
+
712
+ // alert errors
713
+ if (config.showErrors === 'alert') {
714
+ if (obj) {
715
+ errorText += '\n\n' + JSON.stringify(obj)
716
+ }
717
+ window.alert(errorText)
718
+ return
719
+ }
720
+
721
+ // custom function
722
+ if (isFunction(config.showErrors)) {
723
+ config.showErrors(code, msg, obj)
724
+ }
725
+ }
726
+
727
+ function setInitialState () {
728
+ currentOrientation = config.orientation
729
+
730
+ // make sure position is valid
731
+ if (config.hasOwnProperty('position')) {
732
+ if (config.position === 'start') {
733
+ currentPosition = deepCopy(START_POSITION)
734
+ } else if (validFen(config.position)) {
735
+ currentPosition = fenToObj(config.position)
736
+ } else if (validPositionObject(config.position)) {
737
+ currentPosition = deepCopy(config.position)
738
+ } else {
739
+ error(
740
+ 7263,
741
+ 'Invalid value passed to config.position.',
742
+ config.position
743
+ )
744
+ }
745
+ }
746
+ }
747
+
748
+ // -------------------------------------------------------------------------
749
+ // DOM Misc
750
+ // -------------------------------------------------------------------------
751
+
752
+ // calculates square size based on the width of the container
753
+ // got a little CSS black magic here, so let me explain:
754
+ // get the width of the container element (could be anything), reduce by 1 for
755
+ // fudge factor, and then keep reducing until we find an exact mod 8 for
756
+ // our square size
757
+ function calculateSquareSize () {
758
+ var containerWidth = parseInt($container.width(), 10)
759
+
760
+ // defensive, prevent infinite loop
761
+ if (!containerWidth || containerWidth <= 0) {
762
+ return 0
763
+ }
764
+
765
+ // pad one pixel
766
+ var boardWidth = containerWidth - 1
767
+
768
+ while (boardWidth % 8 !== 0 && boardWidth > 0) {
769
+ boardWidth = boardWidth - 1
770
+ }
771
+
772
+ return boardWidth / 8
773
+ }
774
+
775
+ // create random IDs for elements
776
+ function createElIds () {
777
+ // squares on the board
778
+ for (var i = 0; i < COLUMNS.length; i++) {
779
+ for (var j = 1; j <= 8; j++) {
780
+ var square = COLUMNS[i] + j
781
+ squareElsIds[square] = square + '-' + uuid()
782
+ }
783
+ }
784
+
785
+ // spare pieces
786
+ var pieces = 'KQRNBP'.split('')
787
+ for (i = 0; i < pieces.length; i++) {
788
+ var whitePiece = 'w' + pieces[i]
789
+ var blackPiece = 'b' + pieces[i]
790
+ sparePiecesElsIds[whitePiece] = whitePiece + '-' + uuid()
791
+ sparePiecesElsIds[blackPiece] = blackPiece + '-' + uuid()
792
+ }
793
+ }
794
+
795
+ // -------------------------------------------------------------------------
796
+ // Markup Building
797
+ // -------------------------------------------------------------------------
798
+
799
+ function buildBoardHTML (orientation) {
800
+ if (orientation !== 'black') {
801
+ orientation = 'white'
802
+ }
803
+
804
+ var html = ''
805
+
806
+ // algebraic notation / orientation
807
+ var alpha = deepCopy(COLUMNS)
808
+ var row = 8
809
+ if (orientation === 'black') {
810
+ alpha.reverse()
811
+ row = 1
812
+ }
813
+
814
+ var squareColor = 'white'
815
+ for (var i = 0; i < 8; i++) {
816
+ html += '<div class="{row}">'
817
+ for (var j = 0; j < 8; j++) {
818
+ var square = alpha[j] + row
819
+
820
+ html += '<div class="{square} ' + CSS[squareColor] + ' ' +
821
+ 'square-' + square + '" ' +
822
+ 'style="width:' + squareSize + 'px;height:' + squareSize + 'px;" ' +
823
+ 'id="' + squareElsIds[square] + '" ' +
824
+ 'data-square="' + square + '">'
825
+
826
+ if (config.showNotation) {
827
+ // alpha notation
828
+ if ((orientation === 'white' && row === 1) ||
829
+ (orientation === 'black' && row === 8)) {
830
+ html += '<div class="{notation} {alpha}">' + alpha[j] + '</div>'
831
+ }
832
+
833
+ // numeric notation
834
+ if (j === 0) {
835
+ html += '<div class="{notation} {numeric}">' + row + '</div>'
836
+ }
837
+ }
838
+
839
+ html += '</div>' // end .square
840
+
841
+ squareColor = (squareColor === 'white') ? 'black' : 'white'
842
+ }
843
+ html += '<div class="{clearfix}"></div></div>'
844
+
845
+ squareColor = (squareColor === 'white') ? 'black' : 'white'
846
+
847
+ if (orientation === 'white') {
848
+ row = row - 1
849
+ } else {
850
+ row = row + 1
851
+ }
852
+ }
853
+
854
+ return interpolateTemplate(html, CSS)
855
+ }
856
+
857
+ function buildPieceImgSrc (piece) {
858
+ if (isFunction(config.pieceTheme)) {
859
+ return config.pieceTheme(piece)
860
+ }
861
+
862
+ if (isString(config.pieceTheme)) {
863
+ return interpolateTemplate(config.pieceTheme, {piece: piece})
864
+ }
865
+
866
+ // NOTE: this should never happen
867
+ error(8272, 'Unable to build image source for config.pieceTheme.')
868
+ return ''
869
+ }
870
+
871
+ function buildPieceHTML (piece, hidden, id) {
872
+ var html = '<img src="' + buildPieceImgSrc(piece) + '" '
873
+ if (isString(id) && id !== '') {
874
+ html += 'id="' + id + '" '
875
+ }
876
+ html += 'alt="" ' +
877
+ 'class="{piece}" ' +
878
+ 'data-piece="' + piece + '" ' +
879
+ 'style="width:' + squareSize + 'px;' + 'height:' + squareSize + 'px;'
880
+
881
+ if (hidden) {
882
+ html += 'display:none;'
883
+ }
884
+
885
+ html += '" />'
886
+
887
+ return interpolateTemplate(html, CSS)
888
+ }
889
+
890
+ function buildSparePiecesHTML (color) {
891
+ var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP']
892
+ if (color === 'black') {
893
+ pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP']
894
+ }
895
+
896
+ var html = ''
897
+ for (var i = 0; i < pieces.length; i++) {
898
+ html += buildPieceHTML(pieces[i], false, sparePiecesElsIds[pieces[i]])
899
+ }
900
+
901
+ return html
902
+ }
903
+
904
+ // -------------------------------------------------------------------------
905
+ // Animations
906
+ // -------------------------------------------------------------------------
907
+
908
+ function animateSquareToSquare (src, dest, piece, completeFn) {
909
+ // get information about the source and destination squares
910
+ var $srcSquare = $('#' + squareElsIds[src])
911
+ var srcSquarePosition = $srcSquare.offset()
912
+ var $destSquare = $('#' + squareElsIds[dest])
913
+ var destSquarePosition = $destSquare.offset()
914
+
915
+ // create the animated piece and absolutely position it
916
+ // over the source square
917
+ var animatedPieceId = uuid()
918
+ $('body').append(buildPieceHTML(piece, true, animatedPieceId))
919
+ var $animatedPiece = $('#' + animatedPieceId)
920
+ $animatedPiece.css({
921
+ display: '',
922
+ position: 'absolute',
923
+ top: srcSquarePosition.top,
924
+ left: srcSquarePosition.left
925
+ })
926
+
927
+ // remove original piece from source square
928
+ $srcSquare.find('.' + CSS.piece).remove()
929
+
930
+ function onFinishAnimation1 () {
931
+ // add the "real" piece to the destination square
932
+ $destSquare.append(buildPieceHTML(piece))
933
+
934
+ // remove the animated piece
935
+ $animatedPiece.remove()
936
+
937
+ // run complete function
938
+ if (isFunction(completeFn)) {
939
+ completeFn()
940
+ }
941
+ }
942
+
943
+ // animate the piece to the destination square
944
+ var opts = {
945
+ duration: config.moveSpeed,
946
+ complete: onFinishAnimation1
947
+ }
948
+ $animatedPiece.animate(destSquarePosition, opts)
949
+ }
950
+
951
+ function animateSparePieceToSquare (piece, dest, completeFn) {
952
+ var srcOffset = $('#' + sparePiecesElsIds[piece]).offset()
953
+ var $destSquare = $('#' + squareElsIds[dest])
954
+ var destOffset = $destSquare.offset()
955
+
956
+ // create the animate piece
957
+ var pieceId = uuid()
958
+ $('body').append(buildPieceHTML(piece, true, pieceId))
959
+ var $animatedPiece = $('#' + pieceId)
960
+ $animatedPiece.css({
961
+ display: '',
962
+ position: 'absolute',
963
+ left: srcOffset.left,
964
+ top: srcOffset.top
965
+ })
966
+
967
+ // on complete
968
+ function onFinishAnimation2 () {
969
+ // add the "real" piece to the destination square
970
+ $destSquare.find('.' + CSS.piece).remove()
971
+ $destSquare.append(buildPieceHTML(piece))
972
+
973
+ // remove the animated piece
974
+ $animatedPiece.remove()
975
+
976
+ // run complete function
977
+ if (isFunction(completeFn)) {
978
+ completeFn()
979
+ }
980
+ }
981
+
982
+ // animate the piece to the destination square
983
+ var opts = {
984
+ duration: config.moveSpeed,
985
+ complete: onFinishAnimation2
986
+ }
987
+ $animatedPiece.animate(destOffset, opts)
988
+ }
989
+
990
+ // execute an array of animations
991
+ function doAnimations (animations, oldPos, newPos) {
992
+ if (animations.length === 0) return
993
+
994
+ var numFinished = 0
995
+ function onFinishAnimation3 () {
996
+ // exit if all the animations aren't finished
997
+ numFinished = numFinished + 1
998
+ if (numFinished !== animations.length) return
999
+
1000
+ drawPositionInstant()
1001
+
1002
+ // run their onMoveEnd function
1003
+ if (isFunction(config.onMoveEnd)) {
1004
+ config.onMoveEnd(deepCopy(oldPos), deepCopy(newPos))
1005
+ }
1006
+ }
1007
+
1008
+ for (var i = 0; i < animations.length; i++) {
1009
+ var animation = animations[i]
1010
+
1011
+ // clear a piece
1012
+ if (animation.type === 'clear') {
1013
+ $('#' + squareElsIds[animation.square] + ' .' + CSS.piece)
1014
+ .fadeOut(config.trashSpeed, onFinishAnimation3)
1015
+
1016
+ // add a piece with no spare pieces - fade the piece onto the square
1017
+ } else if (animation.type === 'add' && !config.sparePieces) {
1018
+ $('#' + squareElsIds[animation.square])
1019
+ .append(buildPieceHTML(animation.piece, true))
1020
+ .find('.' + CSS.piece)
1021
+ .fadeIn(config.appearSpeed, onFinishAnimation3)
1022
+
1023
+ // add a piece with spare pieces - animate from the spares
1024
+ } else if (animation.type === 'add' && config.sparePieces) {
1025
+ animateSparePieceToSquare(animation.piece, animation.square, onFinishAnimation3)
1026
+
1027
+ // move a piece from squareA to squareB
1028
+ } else if (animation.type === 'move') {
1029
+ animateSquareToSquare(animation.source, animation.destination, animation.piece, onFinishAnimation3)
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ // calculate an array of animations that need to happen in order to get
1035
+ // from pos1 to pos2
1036
+ function calculateAnimations (pos1, pos2) {
1037
+ // make copies of both
1038
+ pos1 = deepCopy(pos1)
1039
+ pos2 = deepCopy(pos2)
1040
+
1041
+ var animations = []
1042
+ var squaresMovedTo = {}
1043
+
1044
+ // remove pieces that are the same in both positions
1045
+ for (var i in pos2) {
1046
+ if (!pos2.hasOwnProperty(i)) continue
1047
+
1048
+ if (pos1.hasOwnProperty(i) && pos1[i] === pos2[i]) {
1049
+ delete pos1[i]
1050
+ delete pos2[i]
1051
+ }
1052
+ }
1053
+
1054
+ // find all the "move" animations
1055
+ for (i in pos2) {
1056
+ if (!pos2.hasOwnProperty(i)) continue
1057
+
1058
+ var closestPiece = findClosestPiece(pos1, pos2[i], i)
1059
+ if (closestPiece) {
1060
+ animations.push({
1061
+ type: 'move',
1062
+ source: closestPiece,
1063
+ destination: i,
1064
+ piece: pos2[i]
1065
+ })
1066
+
1067
+ delete pos1[closestPiece]
1068
+ delete pos2[i]
1069
+ squaresMovedTo[i] = true
1070
+ }
1071
+ }
1072
+
1073
+ // "add" animations
1074
+ for (i in pos2) {
1075
+ if (!pos2.hasOwnProperty(i)) continue
1076
+
1077
+ animations.push({
1078
+ type: 'add',
1079
+ square: i,
1080
+ piece: pos2[i]
1081
+ })
1082
+
1083
+ delete pos2[i]
1084
+ }
1085
+
1086
+ // "clear" animations
1087
+ for (i in pos1) {
1088
+ if (!pos1.hasOwnProperty(i)) continue
1089
+
1090
+ // do not clear a piece if it is on a square that is the result
1091
+ // of a "move", ie: a piece capture
1092
+ if (squaresMovedTo.hasOwnProperty(i)) continue
1093
+
1094
+ animations.push({
1095
+ type: 'clear',
1096
+ square: i,
1097
+ piece: pos1[i]
1098
+ })
1099
+
1100
+ delete pos1[i]
1101
+ }
1102
+
1103
+ return animations
1104
+ }
1105
+
1106
+ // -------------------------------------------------------------------------
1107
+ // Control Flow
1108
+ // -------------------------------------------------------------------------
1109
+
1110
+ function drawPositionInstant () {
1111
+ // clear the board
1112
+ $board.find('.' + CSS.piece).remove()
1113
+
1114
+ // add the pieces
1115
+ for (var i in currentPosition) {
1116
+ if (!currentPosition.hasOwnProperty(i)) continue
1117
+
1118
+ $('#' + squareElsIds[i]).append(buildPieceHTML(currentPosition[i]))
1119
+ }
1120
+ }
1121
+
1122
+ function drawBoard () {
1123
+ $board.html(buildBoardHTML(currentOrientation, squareSize, config.showNotation))
1124
+ drawPositionInstant()
1125
+
1126
+ if (config.sparePieces) {
1127
+ if (currentOrientation === 'white') {
1128
+ $sparePiecesTop.html(buildSparePiecesHTML('black'))
1129
+ $sparePiecesBottom.html(buildSparePiecesHTML('white'))
1130
+ } else {
1131
+ $sparePiecesTop.html(buildSparePiecesHTML('white'))
1132
+ $sparePiecesBottom.html(buildSparePiecesHTML('black'))
1133
+ }
1134
+ }
1135
+ }
1136
+
1137
+ function setCurrentPosition (position) {
1138
+ var oldPos = deepCopy(currentPosition)
1139
+ var newPos = deepCopy(position)
1140
+ var oldFen = objToFen(oldPos)
1141
+ var newFen = objToFen(newPos)
1142
+
1143
+ // do nothing if no change in position
1144
+ if (oldFen === newFen) return
1145
+
1146
+ // run their onChange function
1147
+ if (isFunction(config.onChange)) {
1148
+ config.onChange(oldPos, newPos)
1149
+ }
1150
+
1151
+ // update state
1152
+ currentPosition = position
1153
+ }
1154
+
1155
+ function isXYOnSquare (x, y) {
1156
+ for (var i in squareElsOffsets) {
1157
+ if (!squareElsOffsets.hasOwnProperty(i)) continue
1158
+
1159
+ var s = squareElsOffsets[i]
1160
+ if (x >= s.left &&
1161
+ x < s.left + squareSize &&
1162
+ y >= s.top &&
1163
+ y < s.top + squareSize) {
1164
+ return i
1165
+ }
1166
+ }
1167
+
1168
+ return 'offboard'
1169
+ }
1170
+
1171
+ // records the XY coords of every square into memory
1172
+ function captureSquareOffsets () {
1173
+ squareElsOffsets = {}
1174
+
1175
+ for (var i in squareElsIds) {
1176
+ if (!squareElsIds.hasOwnProperty(i)) continue
1177
+
1178
+ squareElsOffsets[i] = $('#' + squareElsIds[i]).offset()
1179
+ }
1180
+ }
1181
+
1182
+ function removeSquareHighlights () {
1183
+ $board
1184
+ .find('.' + CSS.square)
1185
+ .removeClass(CSS.highlight1 + ' ' + CSS.highlight2)
1186
+ }
1187
+
1188
+ function snapbackDraggedPiece () {
1189
+ // there is no "snapback" for spare pieces
1190
+ if (draggedPieceSource === 'spare') {
1191
+ trashDraggedPiece()
1192
+ return
1193
+ }
1194
+
1195
+ removeSquareHighlights()
1196
+
1197
+ // animation complete
1198
+ function complete () {
1199
+ drawPositionInstant()
1200
+ $draggedPiece.css('display', 'none')
1201
+
1202
+ // run their onSnapbackEnd function
1203
+ if (isFunction(config.onSnapbackEnd)) {
1204
+ config.onSnapbackEnd(
1205
+ draggedPiece,
1206
+ draggedPieceSource,
1207
+ deepCopy(currentPosition),
1208
+ currentOrientation
1209
+ )
1210
+ }
1211
+ }
1212
+
1213
+ // get source square position
1214
+ var sourceSquarePosition = $('#' + squareElsIds[draggedPieceSource]).offset()
1215
+
1216
+ // animate the piece to the target square
1217
+ var opts = {
1218
+ duration: config.snapbackSpeed,
1219
+ complete: complete
1220
+ }
1221
+ $draggedPiece.animate(sourceSquarePosition, opts)
1222
+
1223
+ // set state
1224
+ isDragging = false
1225
+ }
1226
+
1227
+ function trashDraggedPiece () {
1228
+ removeSquareHighlights()
1229
+
1230
+ // remove the source piece
1231
+ var newPosition = deepCopy(currentPosition)
1232
+ delete newPosition[draggedPieceSource]
1233
+ setCurrentPosition(newPosition)
1234
+
1235
+ // redraw the position
1236
+ drawPositionInstant()
1237
+
1238
+ // hide the dragged piece
1239
+ $draggedPiece.fadeOut(config.trashSpeed)
1240
+
1241
+ // set state
1242
+ isDragging = false
1243
+ }
1244
+
1245
+ function dropDraggedPieceOnSquare (square) {
1246
+ removeSquareHighlights()
1247
+
1248
+ // update position
1249
+ var newPosition = deepCopy(currentPosition)
1250
+ delete newPosition[draggedPieceSource]
1251
+ newPosition[square] = draggedPiece
1252
+ setCurrentPosition(newPosition)
1253
+
1254
+ // get target square information
1255
+ var targetSquarePosition = $('#' + squareElsIds[square]).offset()
1256
+
1257
+ // animation complete
1258
+ function onAnimationComplete () {
1259
+ drawPositionInstant()
1260
+ $draggedPiece.css('display', 'none')
1261
+
1262
+ // execute their onSnapEnd function
1263
+ if (isFunction(config.onSnapEnd)) {
1264
+ config.onSnapEnd(draggedPieceSource, square, draggedPiece)
1265
+ }
1266
+ }
1267
+
1268
+ // snap the piece to the target square
1269
+ var opts = {
1270
+ duration: config.snapSpeed,
1271
+ complete: onAnimationComplete
1272
+ }
1273
+ $draggedPiece.animate(targetSquarePosition, opts)
1274
+
1275
+ // set state
1276
+ isDragging = false
1277
+ }
1278
+
1279
+ function beginDraggingPiece (source, piece, x, y) {
1280
+ // run their custom onDragStart function
1281
+ // their custom onDragStart function can cancel drag start
1282
+ if (isFunction(config.onDragStart) &&
1283
+ config.onDragStart(source, piece, deepCopy(currentPosition), currentOrientation) === false) {
1284
+ return
1285
+ }
1286
+
1287
+ // set state
1288
+ isDragging = true
1289
+ draggedPiece = piece
1290
+ draggedPieceSource = source
1291
+
1292
+ // if the piece came from spare pieces, location is offboard
1293
+ if (source === 'spare') {
1294
+ draggedPieceLocation = 'offboard'
1295
+ } else {
1296
+ draggedPieceLocation = source
1297
+ }
1298
+
1299
+ // capture the x, y coords of all squares in memory
1300
+ captureSquareOffsets()
1301
+
1302
+ // create the dragged piece
1303
+ $draggedPiece.attr('src', buildPieceImgSrc(piece)).css({
1304
+ display: '',
1305
+ position: 'absolute',
1306
+ left: x - squareSize / 2,
1307
+ top: y - squareSize / 2
1308
+ })
1309
+
1310
+ if (source !== 'spare') {
1311
+ // highlight the source square and hide the piece
1312
+ $('#' + squareElsIds[source])
1313
+ .addClass(CSS.highlight1)
1314
+ .find('.' + CSS.piece)
1315
+ .css('display', 'none')
1316
+ }
1317
+ }
1318
+
1319
+ function updateDraggedPiece (x, y) {
1320
+ // put the dragged piece over the mouse cursor
1321
+ $draggedPiece.css({
1322
+ left: x - squareSize / 2,
1323
+ top: y - squareSize / 2
1324
+ })
1325
+
1326
+ // get location
1327
+ var location = isXYOnSquare(x, y)
1328
+
1329
+ // do nothing if the location has not changed
1330
+ if (location === draggedPieceLocation) return
1331
+
1332
+ // remove highlight from previous square
1333
+ if (validSquare(draggedPieceLocation)) {
1334
+ $('#' + squareElsIds[draggedPieceLocation]).removeClass(CSS.highlight2)
1335
+ }
1336
+
1337
+ // add highlight to new square
1338
+ if (validSquare(location)) {
1339
+ $('#' + squareElsIds[location]).addClass(CSS.highlight2)
1340
+ }
1341
+
1342
+ // run onDragMove
1343
+ if (isFunction(config.onDragMove)) {
1344
+ config.onDragMove(
1345
+ location,
1346
+ draggedPieceLocation,
1347
+ draggedPieceSource,
1348
+ draggedPiece,
1349
+ deepCopy(currentPosition),
1350
+ currentOrientation
1351
+ )
1352
+ }
1353
+
1354
+ // update state
1355
+ draggedPieceLocation = location
1356
+ }
1357
+
1358
+ function stopDraggedPiece (location) {
1359
+ // determine what the action should be
1360
+ var action = 'drop'
1361
+ if (location === 'offboard' && config.dropOffBoard === 'snapback') {
1362
+ action = 'snapback'
1363
+ }
1364
+ if (location === 'offboard' && config.dropOffBoard === 'trash') {
1365
+ action = 'trash'
1366
+ }
1367
+
1368
+ // run their onDrop function, which can potentially change the drop action
1369
+ if (isFunction(config.onDrop)) {
1370
+ var newPosition = deepCopy(currentPosition)
1371
+
1372
+ // source piece is a spare piece and position is off the board
1373
+ // if (draggedPieceSource === 'spare' && location === 'offboard') {...}
1374
+ // position has not changed; do nothing
1375
+
1376
+ // source piece is a spare piece and position is on the board
1377
+ if (draggedPieceSource === 'spare' && validSquare(location)) {
1378
+ // add the piece to the board
1379
+ newPosition[location] = draggedPiece
1380
+ }
1381
+
1382
+ // source piece was on the board and position is off the board
1383
+ if (validSquare(draggedPieceSource) && location === 'offboard') {
1384
+ // remove the piece from the board
1385
+ delete newPosition[draggedPieceSource]
1386
+ }
1387
+
1388
+ // source piece was on the board and position is on the board
1389
+ if (validSquare(draggedPieceSource) && validSquare(location)) {
1390
+ // move the piece
1391
+ delete newPosition[draggedPieceSource]
1392
+ newPosition[location] = draggedPiece
1393
+ }
1394
+
1395
+ var oldPosition = deepCopy(currentPosition)
1396
+
1397
+ var result = config.onDrop(
1398
+ draggedPieceSource,
1399
+ location,
1400
+ draggedPiece,
1401
+ newPosition,
1402
+ oldPosition,
1403
+ currentOrientation
1404
+ )
1405
+ if (result === 'snapback' || result === 'trash') {
1406
+ action = result
1407
+ }
1408
+ }
1409
+
1410
+ // do it!
1411
+ if (action === 'snapback') {
1412
+ snapbackDraggedPiece()
1413
+ } else if (action === 'trash') {
1414
+ trashDraggedPiece()
1415
+ } else if (action === 'drop') {
1416
+ dropDraggedPieceOnSquare(location)
1417
+ }
1418
+ }
1419
+
1420
+ // -------------------------------------------------------------------------
1421
+ // Public Methods
1422
+ // -------------------------------------------------------------------------
1423
+
1424
+ // clear the board
1425
+ widget.clear = function (useAnimation) {
1426
+ widget.position({}, useAnimation)
1427
+ }
1428
+
1429
+ // remove the widget from the page
1430
+ widget.destroy = function () {
1431
+ // remove markup
1432
+ $container.html('')
1433
+ $draggedPiece.remove()
1434
+
1435
+ // remove event handlers
1436
+ $container.unbind()
1437
+ }
1438
+
1439
+ // shorthand method to get the current FEN
1440
+ widget.fen = function () {
1441
+ return widget.position('fen')
1442
+ }
1443
+
1444
+ // flip orientation
1445
+ widget.flip = function () {
1446
+ return widget.orientation('flip')
1447
+ }
1448
+
1449
+ // move pieces
1450
+ // TODO: this method should be variadic as well as accept an array of moves
1451
+ widget.move = function () {
1452
+ // no need to throw an error here; just do nothing
1453
+ // TODO: this should return the current position
1454
+ if (arguments.length === 0) return
1455
+
1456
+ var useAnimation = true
1457
+
1458
+ // collect the moves into an object
1459
+ var moves = {}
1460
+ for (var i = 0; i < arguments.length; i++) {
1461
+ // any "false" to this function means no animations
1462
+ if (arguments[i] === false) {
1463
+ useAnimation = false
1464
+ continue
1465
+ }
1466
+
1467
+ // skip invalid arguments
1468
+ if (!validMove(arguments[i])) {
1469
+ error(2826, 'Invalid move passed to the move method.', arguments[i])
1470
+ continue
1471
+ }
1472
+
1473
+ var tmp = arguments[i].split('-')
1474
+ moves[tmp[0]] = tmp[1]
1475
+ }
1476
+
1477
+ // calculate position from moves
1478
+ var newPos = calculatePositionFromMoves(currentPosition, moves)
1479
+
1480
+ // update the board
1481
+ widget.position(newPos, useAnimation)
1482
+
1483
+ // return the new position object
1484
+ return newPos
1485
+ }
1486
+
1487
+ widget.orientation = function (arg) {
1488
+ // no arguments, return the current orientation
1489
+ if (arguments.length === 0) {
1490
+ return currentOrientation
1491
+ }
1492
+
1493
+ // set to white or black
1494
+ if (arg === 'white' || arg === 'black') {
1495
+ currentOrientation = arg
1496
+ drawBoard()
1497
+ return currentOrientation
1498
+ }
1499
+
1500
+ // flip orientation
1501
+ if (arg === 'flip') {
1502
+ currentOrientation = currentOrientation === 'white' ? 'black' : 'white'
1503
+ drawBoard()
1504
+ return currentOrientation
1505
+ }
1506
+
1507
+ error(5482, 'Invalid value passed to the orientation method.', arg)
1508
+ }
1509
+
1510
+ widget.position = function (position, useAnimation) {
1511
+ // no arguments, return the current position
1512
+ if (arguments.length === 0) {
1513
+ return deepCopy(currentPosition)
1514
+ }
1515
+
1516
+ // get position as FEN
1517
+ if (isString(position) && position.toLowerCase() === 'fen') {
1518
+ return objToFen(currentPosition)
1519
+ }
1520
+
1521
+ // start position
1522
+ if (isString(position) && position.toLowerCase() === 'start') {
1523
+ position = deepCopy(START_POSITION)
1524
+ }
1525
+
1526
+ // convert FEN to position object
1527
+ if (validFen(position)) {
1528
+ position = fenToObj(position)
1529
+ }
1530
+
1531
+ // validate position object
1532
+ if (!validPositionObject(position)) {
1533
+ error(6482, 'Invalid value passed to the position method.', position)
1534
+ return
1535
+ }
1536
+
1537
+ // default for useAnimations is true
1538
+ if (useAnimation !== false) useAnimation = true
1539
+
1540
+ if (useAnimation) {
1541
+ // start the animations
1542
+ var animations = calculateAnimations(currentPosition, position)
1543
+ doAnimations(animations, currentPosition, position)
1544
+
1545
+ // set the new position
1546
+ setCurrentPosition(position)
1547
+ } else {
1548
+ // instant update
1549
+ setCurrentPosition(position)
1550
+ drawPositionInstant()
1551
+ }
1552
+ }
1553
+
1554
+ widget.resize = function () {
1555
+ // calulate the new square size
1556
+ squareSize = calculateSquareSize()
1557
+
1558
+ // set board width
1559
+ $board.css('width', squareSize * 8 + 'px')
1560
+
1561
+ // set drag piece size
1562
+ $draggedPiece.css({
1563
+ height: squareSize,
1564
+ width: squareSize
1565
+ })
1566
+
1567
+ // spare pieces
1568
+ if (config.sparePieces) {
1569
+ $container
1570
+ .find('.' + CSS.sparePieces)
1571
+ .css('paddingLeft', squareSize + boardBorderSize + 'px')
1572
+ }
1573
+
1574
+ // redraw the board
1575
+ drawBoard()
1576
+ }
1577
+
1578
+ // set the starting position
1579
+ widget.start = function (useAnimation) {
1580
+ widget.position('start', useAnimation)
1581
+ }
1582
+
1583
+ // -------------------------------------------------------------------------
1584
+ // Browser Events
1585
+ // -------------------------------------------------------------------------
1586
+
1587
+ function stopDefault (evt) {
1588
+ evt.preventDefault()
1589
+ }
1590
+
1591
+ function mousedownSquare (evt) {
1592
+ // do nothing if we're not draggable
1593
+ if (!config.draggable) return
1594
+
1595
+ // do nothing if there is no piece on this square
1596
+ var square = $(this).attr('data-square')
1597
+ if (!validSquare(square)) return
1598
+ if (!currentPosition.hasOwnProperty(square)) return
1599
+
1600
+ beginDraggingPiece(square, currentPosition[square], evt.pageX, evt.pageY)
1601
+ }
1602
+
1603
+ function touchstartSquare (e) {
1604
+ // do nothing if we're not draggable
1605
+ if (!config.draggable) return
1606
+
1607
+ // do nothing if there is no piece on this square
1608
+ var square = $(this).attr('data-square')
1609
+ if (!validSquare(square)) return
1610
+ if (!currentPosition.hasOwnProperty(square)) return
1611
+
1612
+ e = e.originalEvent
1613
+ beginDraggingPiece(
1614
+ square,
1615
+ currentPosition[square],
1616
+ e.changedTouches[0].pageX,
1617
+ e.changedTouches[0].pageY
1618
+ )
1619
+ }
1620
+
1621
+ function mousedownSparePiece (evt) {
1622
+ // do nothing if sparePieces is not enabled
1623
+ if (!config.sparePieces) return
1624
+
1625
+ var piece = $(this).attr('data-piece')
1626
+
1627
+ beginDraggingPiece('spare', piece, evt.pageX, evt.pageY)
1628
+ }
1629
+
1630
+ function touchstartSparePiece (e) {
1631
+ // do nothing if sparePieces is not enabled
1632
+ if (!config.sparePieces) return
1633
+
1634
+ var piece = $(this).attr('data-piece')
1635
+
1636
+ e = e.originalEvent
1637
+ beginDraggingPiece(
1638
+ 'spare',
1639
+ piece,
1640
+ e.changedTouches[0].pageX,
1641
+ e.changedTouches[0].pageY
1642
+ )
1643
+ }
1644
+
1645
+ function mousemoveWindow (evt) {
1646
+ if (isDragging) {
1647
+ updateDraggedPiece(evt.pageX, evt.pageY)
1648
+ }
1649
+ }
1650
+
1651
+ var throttledMousemoveWindow = throttle(mousemoveWindow, config.dragThrottleRate)
1652
+
1653
+ function touchmoveWindow (evt) {
1654
+ // do nothing if we are not dragging a piece
1655
+ if (!isDragging) return
1656
+
1657
+ // prevent screen from scrolling
1658
+ evt.preventDefault()
1659
+
1660
+ updateDraggedPiece(evt.originalEvent.changedTouches[0].pageX,
1661
+ evt.originalEvent.changedTouches[0].pageY)
1662
+ }
1663
+
1664
+ var throttledTouchmoveWindow = throttle(touchmoveWindow, config.dragThrottleRate)
1665
+
1666
+ function mouseupWindow (evt) {
1667
+ // do nothing if we are not dragging a piece
1668
+ if (!isDragging) return
1669
+
1670
+ // get the location
1671
+ var location = isXYOnSquare(evt.pageX, evt.pageY)
1672
+
1673
+ stopDraggedPiece(location)
1674
+ }
1675
+
1676
+ function touchendWindow (evt) {
1677
+ // do nothing if we are not dragging a piece
1678
+ if (!isDragging) return
1679
+
1680
+ // get the location
1681
+ var location = isXYOnSquare(evt.originalEvent.changedTouches[0].pageX,
1682
+ evt.originalEvent.changedTouches[0].pageY)
1683
+
1684
+ stopDraggedPiece(location)
1685
+ }
1686
+
1687
+ function mouseenterSquare (evt) {
1688
+ // do not fire this event if we are dragging a piece
1689
+ // NOTE: this should never happen, but it's a safeguard
1690
+ if (isDragging) return
1691
+
1692
+ // exit if they did not provide a onMouseoverSquare function
1693
+ if (!isFunction(config.onMouseoverSquare)) return
1694
+
1695
+ // get the square
1696
+ var square = $(evt.currentTarget).attr('data-square')
1697
+
1698
+ // NOTE: this should never happen; defensive
1699
+ if (!validSquare(square)) return
1700
+
1701
+ // get the piece on this square
1702
+ var piece = false
1703
+ if (currentPosition.hasOwnProperty(square)) {
1704
+ piece = currentPosition[square]
1705
+ }
1706
+
1707
+ // execute their function
1708
+ config.onMouseoverSquare(square, piece, deepCopy(currentPosition), currentOrientation)
1709
+ }
1710
+
1711
+ function mouseleaveSquare (evt) {
1712
+ // do not fire this event if we are dragging a piece
1713
+ // NOTE: this should never happen, but it's a safeguard
1714
+ if (isDragging) return
1715
+
1716
+ // exit if they did not provide an onMouseoutSquare function
1717
+ if (!isFunction(config.onMouseoutSquare)) return
1718
+
1719
+ // get the square
1720
+ var square = $(evt.currentTarget).attr('data-square')
1721
+
1722
+ // NOTE: this should never happen; defensive
1723
+ if (!validSquare(square)) return
1724
+
1725
+ // get the piece on this square
1726
+ var piece = false
1727
+ if (currentPosition.hasOwnProperty(square)) {
1728
+ piece = currentPosition[square]
1729
+ }
1730
+
1731
+ // execute their function
1732
+ config.onMouseoutSquare(square, piece, deepCopy(currentPosition), currentOrientation)
1733
+ }
1734
+
1735
+ // -------------------------------------------------------------------------
1736
+ // Initialization
1737
+ // -------------------------------------------------------------------------
1738
+
1739
+ function addEvents () {
1740
+ // prevent "image drag"
1741
+ $('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault)
1742
+
1743
+ // mouse drag pieces
1744
+ $board.on('mousedown', '.' + CSS.square, mousedownSquare)
1745
+ $container.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, mousedownSparePiece)
1746
+
1747
+ // mouse enter / leave square
1748
+ $board
1749
+ .on('mouseenter', '.' + CSS.square, mouseenterSquare)
1750
+ .on('mouseleave', '.' + CSS.square, mouseleaveSquare)
1751
+
1752
+ // piece drag
1753
+ var $window = $(window)
1754
+ $window
1755
+ .on('mousemove', throttledMousemoveWindow)
1756
+ .on('mouseup', mouseupWindow)
1757
+
1758
+ // touch drag pieces
1759
+ if (isTouchDevice()) {
1760
+ $board.on('touchstart', '.' + CSS.square, touchstartSquare)
1761
+ $container.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, touchstartSparePiece)
1762
+ $window
1763
+ .on('touchmove', throttledTouchmoveWindow)
1764
+ .on('touchend', touchendWindow)
1765
+ }
1766
+ }
1767
+
1768
+ function initDOM () {
1769
+ // create unique IDs for all the elements we will create
1770
+ createElIds()
1771
+
1772
+ // build board and save it in memory
1773
+ $container.html(buildContainerHTML(config.sparePieces))
1774
+ $board = $container.find('.' + CSS.board)
1775
+
1776
+ if (config.sparePieces) {
1777
+ $sparePiecesTop = $container.find('.' + CSS.sparePiecesTop)
1778
+ $sparePiecesBottom = $container.find('.' + CSS.sparePiecesBottom)
1779
+ }
1780
+
1781
+ // create the drag piece
1782
+ var draggedPieceId = uuid()
1783
+ $('body').append(buildPieceHTML('wP', true, draggedPieceId))
1784
+ $draggedPiece = $('#' + draggedPieceId)
1785
+
1786
+ // TODO: need to remove this dragged piece element if the board is no
1787
+ // longer in the DOM
1788
+
1789
+ // get the border size
1790
+ boardBorderSize = parseInt($board.css('borderLeftWidth'), 10)
1791
+
1792
+ // set the size and draw the board
1793
+ widget.resize()
1794
+ }
1795
+
1796
+ // -------------------------------------------------------------------------
1797
+ // Initialization
1798
+ // -------------------------------------------------------------------------
1799
+
1800
+ setInitialState()
1801
+ initDOM()
1802
+ addEvents()
1803
+
1804
+ // return the widget object
1805
+ return widget
1806
+ } // end constructor
1807
+
1808
+ // TODO: do module exports here
1809
+ window['Chessboard'] = constructor
1810
+
1811
+ // support legacy ChessBoard name
1812
+ window['ChessBoard'] = window['Chessboard']
1813
+
1814
+ // expose util functions
1815
+ window['Chessboard']['fenToObj'] = fenToObj
1816
+ window['Chessboard']['objToFen'] = objToFen
1817
+ })() // end anonymous wrapper
static/chessboard-1.0.0.min.css ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ /*! chessboard.js v1.0.0 | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */
2
+ .clearfix-7da63{clear:both}.board-b72b1{border:2px solid #404040;box-sizing:content-box}.square-55d63{float:left;position:relative;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.white-1e1d7{background-color:#f0d9b5;color:#b58863}.black-3c85d{background-color:#b58863;color:#f0d9b5}.highlight1-32417,.highlight2-9c5d2{box-shadow:inset 0 0 3px 3px #ff0}.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}
static/chessboard-1.0.0.min.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ /*! chessboard.js v1.0.0 | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */
2
+ !function(){"use strict";var z=window.jQuery,F="abcdefgh".split(""),r=20,A="…",W="1.8.3",e="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",G=pe(e),n=200,t=200,o=60,a=30,i=100,H={};function V(e,r,n){function t(){o=0,a&&(a=!1,s())}var o=0,a=!1,i=[],s=function(){o=window.setTimeout(t,r),e.apply(n,i)};return function(e){i=arguments,o?a=!0:s()}}function Z(){return"xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx".replace(/x/g,function(e){return(16*Math.random()|0).toString(16)})}function _(e){return JSON.parse(JSON.stringify(e))}function s(e){var r=e.split(".");return{major:parseInt(r[0],10),minor:parseInt(r[1],10),patch:parseInt(r[2],10)}}function ee(e,r){for(var n in r)if(r.hasOwnProperty(n))for(var t="{"+n+"}",o=r[n];-1!==e.indexOf(t);)e=e.replace(t,o);return e}function re(e){return"string"==typeof e}function ne(e){return"function"==typeof e}function p(e){return"number"==typeof e&&isFinite(e)&&Math.floor(e)===e}function c(e){return"fast"===e||"slow"===e||!!p(e)&&0<=e}function te(e){if(!re(e))return!1;var r=e.split("-");return 2===r.length&&(oe(r[0])&&oe(r[1]))}function oe(e){return re(e)&&-1!==e.search(/^[a-h][1-8]$/)}function u(e){return re(e)&&-1!==e.search(/^[bw][KQRNBP]$/)}function ae(e){if(!re(e))return!1;var r=(e=function(e){return e.replace(/8/g,"11111111").replace(/7/g,"1111111").replace(/6/g,"111111").replace(/5/g,"11111").replace(/4/g,"1111").replace(/3/g,"111").replace(/2/g,"11")}(e=e.replace(/ .+$/,""))).split("/");if(8!==r.length)return!1;for(var n=0;n<8;n++)if(8!==r[n].length||-1!==r[n].search(/[^kqrnbpKQRNBP1]/))return!1;return!0}function ie(e){if(!z.isPlainObject(e))return!1;for(var r in e)if(e.hasOwnProperty(r)&&(!oe(r)||!u(e[r])))return!1;return!0}function se(){return typeof window.$&&z.fn&&z.fn.jquery&&function(e,r){e=s(e),r=s(r);var n=1e5*e.major*1e5+1e5*e.minor+e.patch;return 1e5*r.major*1e5+1e5*r.minor+r.patch<=n}(z.fn.jquery,W)}function pe(e){if(!ae(e))return!1;for(var r,n=(e=e.replace(/ .+$/,"")).split("/"),t={},o=8,a=0;a<8;a++){for(var i=n[a].split(""),s=0,p=0;p<i.length;p++){if(-1!==i[p].search(/[1-8]/))s+=parseInt(i[p],10);else t[F[s]+o]=(r=i[p]).toLowerCase()===r?"b"+r.toUpperCase():"w"+r.toUpperCase(),s+=1}o-=1}return t}function ce(e){if(!ie(e))return!1;for(var r,n,t="",o=8,a=0;a<8;a++){for(var i=0;i<8;i++){var s=F[i]+o;e.hasOwnProperty(s)?t+=(r=e[s],n=void 0,"w"===(n=r.split(""))[0]?n[1].toUpperCase():n[1].toLowerCase()):t+="1"}7!==a&&(t+="/"),o-=1}return t=function(e){return e.replace(/11111111/g,"8").replace(/1111111/g,"7").replace(/111111/g,"6").replace(/11111/g,"5").replace(/1111/g,"4").replace(/111/g,"3").replace(/11/g,"2")}(t)}function ue(e,r,n){for(var t=function(e){for(var r=[],n=0;n<8;n++)for(var t=0;t<8;t++){var o=F[n]+(t+1);e!==o&&r.push({square:o,distance:(a=e,i=o,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,s=a.split(""),p=F.indexOf(s[0])+1,c=parseInt(s[1],10),u=i.split(""),f=F.indexOf(u[0])+1,d=parseInt(u[1],10),h=Math.abs(p-f),l=Math.abs(c-d),l<=h?h:l)})}var a,i,s,p,c,u,f,d,h,l;r.sort(function(e,r){return e.distance-r.distance});var v=[];for(n=0;n<r.length;n++)v.push(r[n].square);return v}(n),o=0;o<t.length;o++){var a=t[o];if(e.hasOwnProperty(a)&&e[a]===r)return a}return!1}function fe(e){return"black"!==e.orientation&&(e.orientation="white"),!1!==e.showNotation&&(e.showNotation=!0),!0!==e.draggable&&(e.draggable=!1),"trash"!==e.dropOffBoard&&(e.dropOffBoard="snapback"),!0!==e.sparePieces&&(e.sparePieces=!1),e.sparePieces&&(e.draggable=!0),e.hasOwnProperty("pieceTheme")&&(re(e.pieceTheme)||ne(e.pieceTheme))||(e.pieceTheme="img/chesspieces/wikipedia/{piece}.png"),c(e.appearSpeed)||(e.appearSpeed=n),c(e.moveSpeed)||(e.moveSpeed=t),c(e.snapbackSpeed)||(e.snapbackSpeed=o),c(e.snapSpeed)||(e.snapSpeed=a),c(e.trashSpeed)||(e.trashSpeed=i),function(e){return p(e)&&1<=e}(e.dragThrottleRate)||(e.dragThrottleRate=r),e}H.alpha="alpha-d2270",H.black="black-3c85d",H.board="board-b72b1",H.chessboard="chessboard-63f37",H.clearfix="clearfix-7da63",H.highlight1="highlight1-32417",H.highlight2="highlight2-9c5d2",H.notation="notation-322f9",H.numeric="numeric-fc462",H.piece="piece-417db",H.row="row-5277c",H.sparePieces="spare-pieces-7492f",H.sparePiecesBottom="spare-pieces-bottom-ae20f",H.sparePiecesTop="spare-pieces-top-4028b",H.square="square-55d63",H.white="white-1e1d7",window.Chessboard=function(e,f){if(!function(){if(se())return!0;var e="Chessboard Error 1005: Unable to find a valid version of jQuery. Please include jQuery "+W+" or higher on the page\n\nExiting"+A;return window.alert(e),!1}())return null;var n=function(e){if(""===e){var r="Chessboard Error 1001: The first argument to Chessboard() cannot be an empty string.\n\nExiting"+A;return window.alert(r),!1}re(e)&&"#"!==e.charAt(0)&&(e="#"+e);var n=z(e);if(1===n.length)return n;var t="Chessboard Error 1003: The first argument to Chessboard() must be the ID of a DOM node, an ID query selector, or a single DOM node.\n\nExiting"+A;return window.alert(t),!1}(e);if(!n)return null;f=fe(f=function(e){return"start"===e?e={position:_(G)}:ae(e)?e={position:pe(e)}:ie(e)&&(e={position:_(e)}),z.isPlainObject(e)||(e={}),e}(f));var r=null,a=null,t=null,o=null,i={},s=2,p="white",c={},u=null,d=null,h=null,l=!1,v={},g={},w={},b=16;function m(e,r,n){if(!0===f.hasOwnProperty("showErrors")&&!1!==f.showErrors){var t="Chessboard Error "+e+": "+r;return"console"===f.showErrors&&"object"==typeof console&&"function"==typeof console.log?(console.log(t),void(2<=arguments.length&&console.log(n))):"alert"===f.showErrors?(n&&(t+="\n\n"+JSON.stringify(n)),void window.alert(t)):void(ne(f.showErrors)&&f.showErrors(e,r,n))}}function P(e){return ne(f.pieceTheme)?f.pieceTheme(e):re(f.pieceTheme)?ee(f.pieceTheme,{piece:e}):(m(8272,"Unable to build image source for config.pieceTheme."),"")}function y(e,r,n){var t='<img src="'+P(e)+'" ';return re(n)&&""!==n&&(t+='id="'+n+'" '),t+='alt="" class="{piece}" data-piece="'+e+'" style="width:'+b+"px;height:"+b+"px;",r&&(t+="display:none;"),ee(t+='" />',H)}function x(e){var r=["wK","wQ","wR","wB","wN","wP"];"black"===e&&(r=["bK","bQ","bR","bB","bN","bP"]);for(var n="",t=0;t<r.length;t++)n+=y(r[t],!1,v[r[t]]);return n}function O(e,r,n,t){var o=z("#"+g[e]),a=o.offset(),i=z("#"+g[r]),s=i.offset(),p=Z();z("body").append(y(n,!0,p));var c=z("#"+p);c.css({display:"",position:"absolute",top:a.top,left:a.left}),o.find("."+H.piece).remove();var u={duration:f.moveSpeed,complete:function(){i.append(y(n)),c.remove(),ne(t)&&t()}};c.animate(s,u)}function S(e,r,n){var t=z("#"+v[e]).offset(),o=z("#"+g[r]),a=o.offset(),i=Z();z("body").append(y(e,!0,i));var s=z("#"+i);s.css({display:"",position:"absolute",left:t.left,top:t.top});var p={duration:f.moveSpeed,complete:function(){o.find("."+H.piece).remove(),o.append(y(e)),s.remove(),ne(n)&&n()}};s.animate(a,p)}function T(){for(var e in r.find("."+H.piece).remove(),c)c.hasOwnProperty(e)&&z("#"+g[e]).append(y(c[e]))}function q(){r.html(function(e){"black"!==e&&(e="white");var r="",n=_(F),t=8;"black"===e&&(n.reverse(),t=1);for(var o="white",a=0;a<8;a++){r+='<div class="{row}">';for(var i=0;i<8;i++){var s=n[i]+t;r+='<div class="{square} '+H[o]+" square-"+s+'" style="width:'+b+"px;height:"+b+'px;" id="'+g[s]+'" data-square="'+s+'">',f.showNotation&&(("white"===e&&1===t||"black"===e&&8===t)&&(r+='<div class="{notation} {alpha}">'+n[i]+"</div>"),0===i&&(r+='<div class="{notation} {numeric}">'+t+"</div>")),r+="</div>",o="white"===o?"black":"white"}r+='<div class="{clearfix}"></div></div>',o="white"===o?"black":"white","white"===e?t-=1:t+=1}return ee(r,H)}(p,f.showNotation)),T(),f.sparePieces&&("white"===p?(t.html(x("black")),o.html(x("white"))):(t.html(x("white")),o.html(x("black"))))}function k(e){var r=_(c),n=_(e);ce(r)!==ce(n)&&(ne(f.onChange)&&f.onChange(r,n),c=e)}function E(e,r){for(var n in w)if(w.hasOwnProperty(n)){var t=w[n];if(e>=t.left&&e<t.left+b&&r>=t.top&&r<t.top+b)return n}return"offboard"}function C(){r.find("."+H.square).removeClass(H.highlight1+" "+H.highlight2)}function B(){C();var e=_(c);delete e[h],k(e),T(),a.fadeOut(f.trashSpeed),l=!1}function I(e,r,n,t){ne(f.onDragStart)&&!1===f.onDragStart(e,r,_(c),p)||(l=!0,u=r,d="spare"===(h=e)?"offboard":e,function(){for(var e in w={},g)g.hasOwnProperty(e)&&(w[e]=z("#"+g[e]).offset())}(),a.attr("src",P(r)).css({display:"",position:"absolute",left:n-b/2,top:t-b/2}),"spare"!==e&&z("#"+g[e]).addClass(H.highlight1).find("."+H.piece).css("display","none"))}function M(e,r){a.css({left:e-b/2,top:r-b/2});var n=E(e,r);n!==d&&(oe(d)&&z("#"+g[d]).removeClass(H.highlight2),oe(n)&&z("#"+g[n]).addClass(H.highlight2),ne(f.onDragMove)&&f.onDragMove(n,d,h,u,_(c),p),d=n)}function N(e){var r="drop";if("offboard"===e&&"snapback"===f.dropOffBoard&&(r="snapback"),"offboard"===e&&"trash"===f.dropOffBoard&&(r="trash"),ne(f.onDrop)){var n=_(c);"spare"===h&&oe(e)&&(n[e]=u),oe(h)&&"offboard"===e&&delete n[h],oe(h)&&oe(e)&&(delete n[h],n[e]=u);var t=_(c),o=f.onDrop(h,e,u,n,t,p);"snapback"!==o&&"trash"!==o||(r=o)}"snapback"===r?function(){if("spare"!==h){C();var e=z("#"+g[h]).offset(),r={duration:f.snapbackSpeed,complete:function(){T(),a.css("display","none"),ne(f.onSnapbackEnd)&&f.onSnapbackEnd(u,h,_(c),p)}};a.animate(e,r),l=!1}else B()}():"trash"===r?B():"drop"===r&&function(e){C();var r=_(c);delete r[h],r[e]=u,k(r);var n=z("#"+g[e]).offset(),t={duration:f.snapSpeed,complete:function(){T(),a.css("display","none"),ne(f.onSnapEnd)&&f.onSnapEnd(h,e,u)}};a.animate(n,t),l=!1}(e)}function j(e){e.preventDefault()}function D(e){if(f.draggable){var r=z(this).attr("data-square");oe(r)&&c.hasOwnProperty(r)&&I(r,c[r],e.pageX,e.pageY)}}function R(e){if(f.draggable){var r=z(this).attr("data-square");oe(r)&&c.hasOwnProperty(r)&&(e=e.originalEvent,I(r,c[r],e.changedTouches[0].pageX,e.changedTouches[0].pageY))}}function Q(e){f.sparePieces&&I("spare",z(this).attr("data-piece"),e.pageX,e.pageY)}function X(e){f.sparePieces&&I("spare",z(this).attr("data-piece"),(e=e.originalEvent).changedTouches[0].pageX,e.changedTouches[0].pageY)}i.clear=function(e){i.position({},e)},i.destroy=function(){n.html(""),a.remove(),n.unbind()},i.fen=function(){return i.position("fen")},i.flip=function(){return i.orientation("flip")},i.move=function(){if(0!==arguments.length){for(var e=!0,r={},n=0;n<arguments.length;n++)if(!1!==arguments[n])if(te(arguments[n])){var t=arguments[n].split("-");r[t[0]]=t[1]}else m(2826,"Invalid move passed to the move method.",arguments[n]);else e=!1;var o=function(e,r){var n=_(e);for(var t in r)if(r.hasOwnProperty(t)&&n.hasOwnProperty(t)){var o=n[t];delete n[t],n[r[t]]=o}return n}(c,r);return i.position(o,e),o}},i.orientation=function(e){return 0===arguments.length?p:"white"===e||"black"===e?(p=e,q(),p):"flip"===e?(p="white"===p?"black":"white",q(),p):void m(5482,"Invalid value passed to the orientation method.",e)},i.position=function(e,r){if(0===arguments.length)return _(c);if(re(e)&&"fen"===e.toLowerCase())return ce(c);(re(e)&&"start"===e.toLowerCase()&&(e=_(G)),ae(e)&&(e=pe(e)),ie(e))?(!1!==r&&(r=!0),r?(function(e,r,n){if(0!==e.length)for(var t=0,o=0;o<e.length;o++){var a=e[o];"clear"===a.type?z("#"+g[a.square]+" ."+H.piece).fadeOut(f.trashSpeed,i):"add"!==a.type||f.sparePieces?"add"===a.type&&f.sparePieces?S(a.piece,a.square,i):"move"===a.type&&O(a.source,a.destination,a.piece,i):z("#"+g[a.square]).append(y(a.piece,!0)).find("."+H.piece).fadeIn(f.appearSpeed,i)}function i(){(t+=1)===e.length&&(T(),ne(f.onMoveEnd)&&f.onMoveEnd(_(r),_(n)))}}(function(e,r){e=_(e),r=_(r);var n=[],t={};for(var o in r)r.hasOwnProperty(o)&&e.hasOwnProperty(o)&&e[o]===r[o]&&(delete e[o],delete r[o]);for(o in r)if(r.hasOwnProperty(o)){var a=ue(e,r[o],o);a&&(n.push({type:"move",source:a,destination:o,piece:r[o]}),delete e[a],delete r[o],t[o]=!0)}for(o in r)r.hasOwnProperty(o)&&(n.push({type:"add",square:o,piece:r[o]}),delete r[o]);for(o in e)e.hasOwnProperty(o)&&(t.hasOwnProperty(o)||(n.push({type:"clear",square:o,piece:e[o]}),delete e[o]));return n}(c,e),c,e),k(e)):(k(e),T())):m(6482,"Invalid value passed to the position method.",e)},i.resize=function(){b=function(){var e=parseInt(n.width(),10);if(!e||e<=0)return 0;for(var r=e-1;r%8!=0&&0<r;)r-=1;return r/8}(),r.css("width",8*b+"px"),a.css({height:b,width:b}),f.sparePieces&&n.find("."+H.sparePieces).css("paddingLeft",b+s+"px"),q()},i.start=function(e){i.position("start",e)};var Y=V(function(e){l&&M(e.pageX,e.pageY)},f.dragThrottleRate),K=V(function(e){l&&(e.preventDefault(),M(e.originalEvent.changedTouches[0].pageX,e.originalEvent.changedTouches[0].pageY))},f.dragThrottleRate);function L(e){l&&N(E(e.pageX,e.pageY))}function U(e){l&&N(E(e.originalEvent.changedTouches[0].pageX,e.originalEvent.changedTouches[0].pageY))}function $(e){if(!l&&ne(f.onMouseoverSquare)){var r=z(e.currentTarget).attr("data-square");if(oe(r)){var n=!1;c.hasOwnProperty(r)&&(n=c[r]),f.onMouseoverSquare(r,n,_(c),p)}}}function J(e){if(!l&&ne(f.onMouseoutSquare)){var r=z(e.currentTarget).attr("data-square");if(oe(r)){var n=!1;c.hasOwnProperty(r)&&(n=c[r]),f.onMouseoutSquare(r,n,_(c),p)}}}return p=f.orientation,f.hasOwnProperty("position")&&("start"===f.position?c=_(G):ae(f.position)?c=pe(f.position):ie(f.position)?c=_(f.position):m(7263,"Invalid value passed to config.position.",f.position)),function(){!function(){for(var e=0;e<F.length;e++)for(var r=1;r<=8;r++){var n=F[e]+r;g[n]=n+"-"+Z()}var t="KQRNBP".split("");for(e=0;e<t.length;e++){var o="w"+t[e],a="b"+t[e];v[o]=o+"-"+Z(),v[a]=a+"-"+Z()}}(),n.html(function(e){var r='<div class="{chessboard}">';return e&&(r+='<div class="{sparePieces} {sparePiecesTop}"></div>'),r+='<div class="{board}"></div>',e&&(r+='<div class="{sparePieces} {sparePiecesBottom}"></div>'),ee(r+="</div>",H)}(f.sparePieces)),r=n.find("."+H.board),f.sparePieces&&(t=n.find("."+H.sparePiecesTop),o=n.find("."+H.sparePiecesBottom));var e=Z();z("body").append(y("wP",!0,e)),a=z("#"+e),s=parseInt(r.css("borderLeftWidth"),10),i.resize()}(),function(){z("body").on("mousedown mousemove","."+H.piece,j),r.on("mousedown","."+H.square,D),n.on("mousedown","."+H.sparePieces+" ."+H.piece,Q),r.on("mouseenter","."+H.square,$).on("mouseleave","."+H.square,J);var e=z(window);e.on("mousemove",Y).on("mouseup",L),"ontouchstart"in document.documentElement&&(r.on("touchstart","."+H.square,R),n.on("touchstart","."+H.sparePieces+" ."+H.piece,X),e.on("touchmove",K).on("touchend",U))}(),i},window.ChessBoard=window.Chessboard,window.Chessboard.fenToObj=pe,window.Chessboard.objToFen=ce}();
static/img/chesspieces/wikipedia/bB.png ADDED
static/img/chesspieces/wikipedia/bK.png ADDED
static/img/chesspieces/wikipedia/bN.png ADDED
static/img/chesspieces/wikipedia/bP.png ADDED
static/img/chesspieces/wikipedia/bQ.png ADDED
static/img/chesspieces/wikipedia/bR.png ADDED
static/img/chesspieces/wikipedia/wB.png ADDED
static/img/chesspieces/wikipedia/wK.png ADDED
static/img/chesspieces/wikipedia/wN.png ADDED
static/img/chesspieces/wikipedia/wP.png ADDED
static/img/chesspieces/wikipedia/wQ.png ADDED
static/img/chesspieces/wikipedia/wR.png ADDED