├── .DS_Store ├── .gitattributes ├── img └── chesspieces │ └── wikipedia │ ├── bB.png │ ├── bK.png │ ├── bN.png │ ├── bP.png │ ├── bQ.png │ ├── bR.png │ ├── wB.png │ ├── wK.png │ ├── wN.png │ ├── wP.png │ ├── wQ.png │ └── wR.png ├── css └── main.css ├── LICENSE ├── README.md ├── index.html └── js ├── main.js └── chess.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/bB.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/bK.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/bN.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/bP.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/bQ.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/bR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/bR.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/wB.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/wK.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/wN.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/wP.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/wQ.png -------------------------------------------------------------------------------- /img/chesspieces/wikipedia/wR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeyu2001/chess-ai/HEAD/img/chesspieces/wikipedia/wR.png -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | .text-align-center { 2 | text-align: center; 3 | } 4 | 5 | #myBoard { 6 | max-width: 100%; 7 | height: auto; 8 | overflow: scroll; 9 | } 10 | 11 | .highlight-white { 12 | box-shadow: inset 0 0 3px 3px yellow; 13 | } 14 | 15 | .highlight-black { 16 | box-shadow: inset 0 0 3px 3px blue; 17 | } 18 | 19 | .highlight-hint { 20 | box-shadow: inset 0 0 3px 3px red; 21 | } 22 | 23 | .no-underline { 24 | text-decoration: none; 25 | } 26 | 27 | .no-outline:focus { 28 | outline: none; 29 | box-shadow: none; 30 | } 31 | 32 | .btn-header { 33 | font-size: 20px; 34 | font-weight: bold; 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Zhang Zeyu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chess-ai 2 | A chess engine by someone who doesn't know how to play chess. 3 | 4 | ## About 5 | chess-ai is a simple chess AI in JavaScript. 6 | 7 | The primary concern of chess-ai is the decision-making part of the application. All functionality outside the scope of the AI are implemented using external libraries: 8 | - Chessboard GUI: Using the chessboard.js API 9 | - Game Mechanics: Using the chess.js API 10 | 11 | The AI uses the [minimax algorithm](https://en.wikipedia.org/wiki/Minimax), which is optimised by [alpha-beta pruning](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning). 12 | 13 | The evaluation function uses [piece square tables](https://www.chessprogramming.org/Piece-Square_Tables) adapted from Sunfish.py, and eliminates the need for nested loops by updating the sum based on each move instead of re-computing the sum of individual pieces at each leaf node. 14 | 15 | A global sum is used to keep track of black's evaluation score after each move, which is used to display the 'advantage' bar. 16 | 17 | ## How to Play? 18 | 1. Head over to https://zeyu2001.github.io/chess-ai/. 19 | 20 | 2. Play as white by dragging a piece to your desired location. The AI plays as black. The AI's minimax search depth (which is directly related to how well it will play) can be customised using the 'Search Depth (Black)' dropdown. Using a higher value will improve the AI's accuracy, but it will take longer to decide on the next move. 21 | 22 | 3. To pit the AI against itself, click the 'Start Game' button under Computer vs. Computer. You can stop the game at any time using the 'Stop and Reset' button. 23 | 24 | ## License 25 | Use of this project is governed by the [MIT License](LICENSE). 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chess AI 5 | 6 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 57 | 58 |
59 |
60 |
61 |
62 |

Simple Chess AI

63 |
64 | 0   positions evaluated in   0s. 65 |
66 |
67 | That's   0   positions / s. 68 |
69 |
70 |
71 |
72 |

73 | 76 |

77 |
78 |
79 |
80 |
81 |
82 |
83 | 84 | 91 |
92 |
93 |
94 |
95 | 96 | 103 |
104 |
105 |
106 |
107 | 108 | 109 |
110 |
111 |
112 |
113 |
114 |
115 |

116 | 119 |

120 |
121 |
122 |
123 |
124 |
125 |
126 | 127 |
128 |
129 | 130 |
131 |
132 |
133 |
134 | 135 |
136 |
137 | 138 |
139 |
140 |
141 |
142 |
143 |
144 |

145 | 148 |

149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | 157 |
158 |
159 | 160 |
161 |
162 |
163 |
164 |
165 |
166 |

Advantage

167 |

Neither side has the advantage 168 | (+0).

169 |
170 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |

Status

180 |

No check, checkmate, or draw.

181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | 189 |
190 |
191 | 192 |
193 |
194 |
195 |
196 |
197 |
198 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A simple chess AI, by someone who doesn't know how to play chess. 3 | * Uses the chessboard.js and chess.js libraries. 4 | * 5 | * Copyright (c) 2020 Zhang Zeyu 6 | */ 7 | 8 | var STACK_SIZE = 100; // maximum size of undo stack 9 | 10 | var board = null; 11 | var $board = $('#myBoard'); 12 | var game = new Chess(); 13 | var globalSum = 0; // always from black's perspective. Negative for white's perspective. 14 | var whiteSquareGrey = '#a9a9a9'; 15 | var blackSquareGrey = '#696969'; 16 | 17 | var squareClass = 'square-55d63'; 18 | var squareToHighlight = null; 19 | var colorToHighlight = null; 20 | var positionCount; 21 | 22 | var config = { 23 | draggable: true, 24 | position: 'start', 25 | onDragStart: onDragStart, 26 | onDrop: onDrop, 27 | onMouseoutSquare: onMouseoutSquare, 28 | onMouseoverSquare: onMouseoverSquare, 29 | onSnapEnd: onSnapEnd, 30 | }; 31 | board = Chessboard('myBoard', config); 32 | 33 | timer = null; 34 | 35 | /* 36 | * Piece Square Tables, adapted from Sunfish.py: 37 | * https://github.com/thomasahle/sunfish/blob/master/sunfish.py 38 | */ 39 | 40 | var weights = { p: 100, n: 280, b: 320, r: 479, q: 929, k: 60000, k_e: 60000 }; 41 | var pst_w = { 42 | p: [ 43 | [100, 100, 100, 100, 105, 100, 100, 100], 44 | [78, 83, 86, 73, 102, 82, 85, 90], 45 | [7, 29, 21, 44, 40, 31, 44, 7], 46 | [-17, 16, -2, 15, 14, 0, 15, -13], 47 | [-26, 3, 10, 9, 6, 1, 0, -23], 48 | [-22, 9, 5, -11, -10, -2, 3, -19], 49 | [-31, 8, -7, -37, -36, -14, 3, -31], 50 | [0, 0, 0, 0, 0, 0, 0, 0], 51 | ], 52 | n: [ 53 | [-66, -53, -75, -75, -10, -55, -58, -70], 54 | [-3, -6, 100, -36, 4, 62, -4, -14], 55 | [10, 67, 1, 74, 73, 27, 62, -2], 56 | [24, 24, 45, 37, 33, 41, 25, 17], 57 | [-1, 5, 31, 21, 22, 35, 2, 0], 58 | [-18, 10, 13, 22, 18, 15, 11, -14], 59 | [-23, -15, 2, 0, 2, 0, -23, -20], 60 | [-74, -23, -26, -24, -19, -35, -22, -69], 61 | ], 62 | b: [ 63 | [-59, -78, -82, -76, -23, -107, -37, -50], 64 | [-11, 20, 35, -42, -39, 31, 2, -22], 65 | [-9, 39, -32, 41, 52, -10, 28, -14], 66 | [25, 17, 20, 34, 26, 25, 15, 10], 67 | [13, 10, 17, 23, 17, 16, 0, 7], 68 | [14, 25, 24, 15, 8, 25, 20, 15], 69 | [19, 20, 11, 6, 7, 6, 20, 16], 70 | [-7, 2, -15, -12, -14, -15, -10, -10], 71 | ], 72 | r: [ 73 | [35, 29, 33, 4, 37, 33, 56, 50], 74 | [55, 29, 56, 67, 55, 62, 34, 60], 75 | [19, 35, 28, 33, 45, 27, 25, 15], 76 | [0, 5, 16, 13, 18, -4, -9, -6], 77 | [-28, -35, -16, -21, -13, -29, -46, -30], 78 | [-42, -28, -42, -25, -25, -35, -26, -46], 79 | [-53, -38, -31, -26, -29, -43, -44, -53], 80 | [-30, -24, -18, 5, -2, -18, -31, -32], 81 | ], 82 | q: [ 83 | [6, 1, -8, -104, 69, 24, 88, 26], 84 | [14, 32, 60, -10, 20, 76, 57, 24], 85 | [-2, 43, 32, 60, 72, 63, 43, 2], 86 | [1, -16, 22, 17, 25, 20, -13, -6], 87 | [-14, -15, -2, -5, -1, -10, -20, -22], 88 | [-30, -6, -13, -11, -16, -11, -16, -27], 89 | [-36, -18, 0, -19, -15, -15, -21, -38], 90 | [-39, -30, -31, -13, -31, -36, -34, -42], 91 | ], 92 | k: [ 93 | [4, 54, 47, -99, -99, 60, 83, -62], 94 | [-32, 10, 55, 56, 56, 55, 10, 3], 95 | [-62, 12, -57, 44, -67, 28, 37, -31], 96 | [-55, 50, 11, -4, -19, 13, 0, -49], 97 | [-55, -43, -52, -28, -51, -47, -8, -50], 98 | [-47, -42, -43, -79, -64, -32, -29, -32], 99 | [-4, 3, -14, -50, -57, -18, 13, 4], 100 | [17, 30, -3, -14, 6, -1, 40, 18], 101 | ], 102 | 103 | // Endgame King Table 104 | k_e: [ 105 | [-50, -40, -30, -20, -20, -30, -40, -50], 106 | [-30, -20, -10, 0, 0, -10, -20, -30], 107 | [-30, -10, 20, 30, 30, 20, -10, -30], 108 | [-30, -10, 30, 40, 40, 30, -10, -30], 109 | [-30, -10, 30, 40, 40, 30, -10, -30], 110 | [-30, -10, 20, 30, 30, 20, -10, -30], 111 | [-30, -30, 0, 0, 0, 0, -30, -30], 112 | [-50, -30, -30, -30, -30, -30, -30, -50], 113 | ], 114 | }; 115 | var pst_b = { 116 | p: pst_w['p'].slice().reverse(), 117 | n: pst_w['n'].slice().reverse(), 118 | b: pst_w['b'].slice().reverse(), 119 | r: pst_w['r'].slice().reverse(), 120 | q: pst_w['q'].slice().reverse(), 121 | k: pst_w['k'].slice().reverse(), 122 | k_e: pst_w['k_e'].slice().reverse(), 123 | }; 124 | 125 | var pstOpponent = { w: pst_b, b: pst_w }; 126 | var pstSelf = { w: pst_w, b: pst_b }; 127 | 128 | /* 129 | * Evaluates the board at this point in time, 130 | * using the material weights and piece square tables. 131 | */ 132 | function evaluateBoard(game, move, prevSum, color) { 133 | 134 | if (game.in_checkmate()) { 135 | 136 | // Opponent is in checkmate (good for us) 137 | if (move.color === color) { 138 | return 10 ** 10; 139 | } 140 | // Our king's in checkmate (bad for us) 141 | else { 142 | return -(10 ** 10); 143 | } 144 | } 145 | 146 | if (game.in_draw() || game.in_threefold_repetition() || game.in_stalemate()) 147 | { 148 | return 0; 149 | } 150 | 151 | if (game.in_check()) { 152 | // Opponent is in check (good for us) 153 | if (move.color === color) { 154 | prevSum += 50; 155 | } 156 | // Our king's in check (bad for us) 157 | else { 158 | prevSum -= 50; 159 | } 160 | } 161 | 162 | var from = [ 163 | 8 - parseInt(move.from[1]), 164 | move.from.charCodeAt(0) - 'a'.charCodeAt(0), 165 | ]; 166 | var to = [ 167 | 8 - parseInt(move.to[1]), 168 | move.to.charCodeAt(0) - 'a'.charCodeAt(0), 169 | ]; 170 | 171 | // Change endgame behavior for kings 172 | if (prevSum < -1500) { 173 | if (move.piece === 'k') { 174 | move.piece = 'k_e'; 175 | } 176 | // Kings can never be captured 177 | // else if (move.captured === 'k') { 178 | // move.captured = 'k_e'; 179 | // } 180 | } 181 | 182 | if ('captured' in move) { 183 | // Opponent piece was captured (good for us) 184 | if (move.color === color) { 185 | prevSum += 186 | weights[move.captured] + 187 | pstOpponent[move.color][move.captured][to[0]][to[1]]; 188 | } 189 | // Our piece was captured (bad for us) 190 | else { 191 | prevSum -= 192 | weights[move.captured] + 193 | pstSelf[move.color][move.captured][to[0]][to[1]]; 194 | } 195 | } 196 | 197 | if (move.flags.includes('p')) { 198 | // NOTE: promote to queen for simplicity 199 | move.promotion = 'q'; 200 | 201 | // Our piece was promoted (good for us) 202 | if (move.color === color) { 203 | prevSum -= 204 | weights[move.piece] + pstSelf[move.color][move.piece][from[0]][from[1]]; 205 | prevSum += 206 | weights[move.promotion] + 207 | pstSelf[move.color][move.promotion][to[0]][to[1]]; 208 | } 209 | // Opponent piece was promoted (bad for us) 210 | else { 211 | prevSum += 212 | weights[move.piece] + pstSelf[move.color][move.piece][from[0]][from[1]]; 213 | prevSum -= 214 | weights[move.promotion] + 215 | pstSelf[move.color][move.promotion][to[0]][to[1]]; 216 | } 217 | } else { 218 | // The moved piece still exists on the updated board, so we only need to update the position value 219 | if (move.color !== color) { 220 | prevSum += pstSelf[move.color][move.piece][from[0]][from[1]]; 221 | prevSum -= pstSelf[move.color][move.piece][to[0]][to[1]]; 222 | } else { 223 | prevSum -= pstSelf[move.color][move.piece][from[0]][from[1]]; 224 | prevSum += pstSelf[move.color][move.piece][to[0]][to[1]]; 225 | } 226 | } 227 | 228 | return prevSum; 229 | } 230 | 231 | /* 232 | * Performs the minimax algorithm to choose the best move: https://en.wikipedia.org/wiki/Minimax (pseudocode provided) 233 | * Recursively explores all possible moves up to a given depth, and evaluates the game board at the leaves. 234 | * 235 | * Basic idea: maximize the minimum value of the position resulting from the opponent's possible following moves. 236 | * Optimization: alpha-beta pruning: https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning (pseudocode provided) 237 | * 238 | * Inputs: 239 | * - game: the game object. 240 | * - depth: the depth of the recursive tree of all possible moves (i.e. height limit). 241 | * - isMaximizingPlayer: true if the current layer is maximizing, false otherwise. 242 | * - sum: the sum (evaluation) so far at the current layer. 243 | * - color: the color of the current player. 244 | * 245 | * Output: 246 | * the best move at the root of the current subtree. 247 | */ 248 | function minimax(game, depth, alpha, beta, isMaximizingPlayer, sum, color) { 249 | positionCount++; 250 | var children = game.ugly_moves({ verbose: true }); 251 | 252 | // Sort moves randomly, so the same move isn't always picked on ties 253 | children.sort(function (a, b) { 254 | return 0.5 - Math.random(); 255 | }); 256 | 257 | var currMove; 258 | // Maximum depth exceeded or node is a terminal node (no children) 259 | if (depth === 0 || children.length === 0) { 260 | return [null, sum]; 261 | } 262 | 263 | // Find maximum/minimum from list of 'children' (possible moves) 264 | var maxValue = Number.NEGATIVE_INFINITY; 265 | var minValue = Number.POSITIVE_INFINITY; 266 | var bestMove; 267 | for (var i = 0; i < children.length; i++) { 268 | currMove = children[i]; 269 | 270 | // Note: in our case, the 'children' are simply modified game states 271 | var currPrettyMove = game.ugly_move(currMove); 272 | var newSum = evaluateBoard(game, currPrettyMove, sum, color); 273 | var [childBestMove, childValue] = minimax( 274 | game, 275 | depth - 1, 276 | alpha, 277 | beta, 278 | !isMaximizingPlayer, 279 | newSum, 280 | color 281 | ); 282 | 283 | game.undo(); 284 | 285 | if (isMaximizingPlayer) { 286 | if (childValue > maxValue) { 287 | maxValue = childValue; 288 | bestMove = currPrettyMove; 289 | } 290 | if (childValue > alpha) { 291 | alpha = childValue; 292 | } 293 | } else { 294 | if (childValue < minValue) { 295 | minValue = childValue; 296 | bestMove = currPrettyMove; 297 | } 298 | if (childValue < beta) { 299 | beta = childValue; 300 | } 301 | } 302 | 303 | // Alpha-beta pruning 304 | if (alpha >= beta) { 305 | break; 306 | } 307 | } 308 | 309 | if (isMaximizingPlayer) { 310 | return [bestMove, maxValue]; 311 | } else { 312 | return [bestMove, minValue]; 313 | } 314 | } 315 | 316 | function checkStatus(color) { 317 | if (game.in_checkmate()) { 318 | $('#status').html(`Checkmate! Oops, ${color} lost.`); 319 | } else if (game.insufficient_material()) { 320 | $('#status').html(`It's a draw! (Insufficient Material)`); 321 | } else if (game.in_threefold_repetition()) { 322 | $('#status').html(`It's a draw! (Threefold Repetition)`); 323 | } else if (game.in_stalemate()) { 324 | $('#status').html(`It's a draw! (Stalemate)`); 325 | } else if (game.in_draw()) { 326 | $('#status').html(`It's a draw! (50-move Rule)`); 327 | } else if (game.in_check()) { 328 | $('#status').html(`Oops, ${color} is in check!`); 329 | return false; 330 | } else { 331 | $('#status').html(`No check, checkmate, or draw.`); 332 | return false; 333 | } 334 | return true; 335 | } 336 | 337 | function updateAdvantage() { 338 | if (globalSum > 0) { 339 | $('#advantageColor').text('Black'); 340 | $('#advantageNumber').text(globalSum); 341 | } else if (globalSum < 0) { 342 | $('#advantageColor').text('White'); 343 | $('#advantageNumber').text(-globalSum); 344 | } else { 345 | $('#advantageColor').text('Neither side'); 346 | $('#advantageNumber').text(globalSum); 347 | } 348 | $('#advantageBar').attr({ 349 | 'aria-valuenow': `${-globalSum}`, 350 | style: `width: ${((-globalSum + 2000) / 4000) * 100}%`, 351 | }); 352 | } 353 | 354 | /* 355 | * Calculates the best legal move for the given color. 356 | */ 357 | function getBestMove(game, color, currSum) { 358 | positionCount = 0; 359 | 360 | if (color === 'b') { 361 | var depth = parseInt($('#search-depth').find(':selected').text()); 362 | } else { 363 | var depth = parseInt($('#search-depth-white').find(':selected').text()); 364 | } 365 | 366 | var d = new Date().getTime(); 367 | var [bestMove, bestMoveValue] = minimax( 368 | game, 369 | depth, 370 | Number.NEGATIVE_INFINITY, 371 | Number.POSITIVE_INFINITY, 372 | true, 373 | currSum, 374 | color 375 | ); 376 | var d2 = new Date().getTime(); 377 | var moveTime = d2 - d; 378 | var positionsPerS = (positionCount * 1000) / moveTime; 379 | 380 | $('#position-count').text(positionCount); 381 | $('#time').text(moveTime / 1000); 382 | $('#positions-per-s').text(Math.round(positionsPerS)); 383 | 384 | return [bestMove, bestMoveValue]; 385 | } 386 | 387 | /* 388 | * Makes the best legal move for the given color. 389 | */ 390 | function makeBestMove(color) { 391 | if (color === 'b') { 392 | var move = getBestMove(game, color, globalSum)[0]; 393 | } else { 394 | var move = getBestMove(game, color, -globalSum)[0]; 395 | } 396 | 397 | globalSum = evaluateBoard(game, move, globalSum, 'b'); 398 | updateAdvantage(); 399 | 400 | game.move(move); 401 | board.position(game.fen()); 402 | 403 | if (color === 'b') { 404 | checkStatus('black'); 405 | 406 | // Highlight black move 407 | $board.find('.' + squareClass).removeClass('highlight-black'); 408 | $board.find('.square-' + move.from).addClass('highlight-black'); 409 | squareToHighlight = move.to; 410 | colorToHighlight = 'black'; 411 | 412 | $board 413 | .find('.square-' + squareToHighlight) 414 | .addClass('highlight-' + colorToHighlight); 415 | } else { 416 | checkStatus('white'); 417 | 418 | // Highlight white move 419 | $board.find('.' + squareClass).removeClass('highlight-white'); 420 | $board.find('.square-' + move.from).addClass('highlight-white'); 421 | squareToHighlight = move.to; 422 | colorToHighlight = 'white'; 423 | 424 | $board 425 | .find('.square-' + squareToHighlight) 426 | .addClass('highlight-' + colorToHighlight); 427 | } 428 | } 429 | 430 | /* 431 | * Plays Computer vs. Computer, starting with a given color. 432 | */ 433 | function compVsComp(color) { 434 | if (!checkStatus({ w: 'white', b: 'black' }[color])) { 435 | timer = window.setTimeout(function () { 436 | makeBestMove(color); 437 | if (color === 'w') { 438 | color = 'b'; 439 | } else { 440 | color = 'w'; 441 | } 442 | compVsComp(color); 443 | }, 250); 444 | } 445 | } 446 | 447 | /* 448 | * Resets the game to its initial state. 449 | */ 450 | function reset() { 451 | game.reset(); 452 | globalSum = 0; 453 | $board.find('.' + squareClass).removeClass('highlight-white'); 454 | $board.find('.' + squareClass).removeClass('highlight-black'); 455 | $board.find('.' + squareClass).removeClass('highlight-hint'); 456 | board.position(game.fen()); 457 | $('#advantageColor').text('Neither side'); 458 | $('#advantageNumber').text(globalSum); 459 | 460 | // Kill the Computer vs. Computer callback 461 | if (timer) { 462 | clearTimeout(timer); 463 | timer = null; 464 | } 465 | } 466 | 467 | /* 468 | * Event listeners for various buttons. 469 | */ 470 | $('#ruyLopezBtn').on('click', function () { 471 | reset(); 472 | game.load( 473 | 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 0 1' 474 | ); 475 | board.position(game.fen()); 476 | window.setTimeout(function () { 477 | makeBestMove('b'); 478 | }, 250); 479 | }); 480 | $('#italianGameBtn').on('click', function () { 481 | reset(); 482 | game.load( 483 | 'r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 0 1' 484 | ); 485 | board.position(game.fen()); 486 | window.setTimeout(function () { 487 | makeBestMove('b'); 488 | }, 250); 489 | }); 490 | $('#sicilianDefenseBtn').on('click', function () { 491 | reset(); 492 | game.load('rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1'); 493 | board.position(game.fen()); 494 | }); 495 | $('#startBtn').on('click', function () { 496 | reset(); 497 | }); 498 | 499 | $('#compVsCompBtn').on('click', function () { 500 | reset(); 501 | compVsComp('w'); 502 | }); 503 | $('#resetBtn').on('click', function () { 504 | reset(); 505 | }); 506 | 507 | var undo_stack = []; 508 | 509 | function undo() { 510 | var move = game.undo(); 511 | undo_stack.push(move); 512 | 513 | // Maintain a maximum stack size 514 | if (undo_stack.length > STACK_SIZE) { 515 | undo_stack.shift(); 516 | } 517 | board.position(game.fen()); 518 | } 519 | 520 | $('#undoBtn').on('click', function () { 521 | if (game.history().length >= 2) { 522 | $board.find('.' + squareClass).removeClass('highlight-white'); 523 | $board.find('.' + squareClass).removeClass('highlight-black'); 524 | $board.find('.' + squareClass).removeClass('highlight-hint'); 525 | 526 | // Undo twice: Opponent's latest move, followed by player's latest move 527 | undo(); 528 | window.setTimeout(function () { 529 | undo(); 530 | window.setTimeout(function () { 531 | showHint(); 532 | }, 250); 533 | }, 250); 534 | } else { 535 | alert('Nothing to undo.'); 536 | } 537 | }); 538 | 539 | function redo() { 540 | game.move(undo_stack.pop()); 541 | board.position(game.fen()); 542 | } 543 | 544 | $('#redoBtn').on('click', function () { 545 | if (undo_stack.length >= 2) { 546 | // Redo twice: Player's last move, followed by opponent's last move 547 | redo(); 548 | window.setTimeout(function () { 549 | redo(); 550 | window.setTimeout(function () { 551 | showHint(); 552 | }, 250); 553 | }, 250); 554 | } else { 555 | alert('Nothing to redo.'); 556 | } 557 | }); 558 | 559 | $('#showHint').change(function () { 560 | window.setTimeout(showHint, 250); 561 | }); 562 | 563 | function showHint() { 564 | var showHint = document.getElementById('showHint'); 565 | $board.find('.' + squareClass).removeClass('highlight-hint'); 566 | 567 | // Show hint (best move for white) 568 | if (showHint.checked) { 569 | var move = getBestMove(game, 'w', -globalSum)[0]; 570 | 571 | $board.find('.square-' + move.from).addClass('highlight-hint'); 572 | $board.find('.square-' + move.to).addClass('highlight-hint'); 573 | } 574 | } 575 | 576 | /* 577 | * The remaining code is adapted from chessboard.js examples #5000 through #5005: 578 | * https://chessboardjs.com/examples#5000 579 | */ 580 | function removeGreySquares() { 581 | $('#myBoard .square-55d63').css('background', ''); 582 | } 583 | 584 | function greySquare(square) { 585 | var $square = $('#myBoard .square-' + square); 586 | 587 | var background = whiteSquareGrey; 588 | if ($square.hasClass('black-3c85d')) { 589 | background = blackSquareGrey; 590 | } 591 | 592 | $square.css('background', background); 593 | } 594 | 595 | function onDragStart(source, piece) { 596 | // do not pick up pieces if the game is over 597 | if (game.game_over()) return false; 598 | 599 | // or if it's not that side's turn 600 | if ( 601 | (game.turn() === 'w' && piece.search(/^b/) !== -1) || 602 | (game.turn() === 'b' && piece.search(/^w/) !== -1) 603 | ) { 604 | return false; 605 | } 606 | } 607 | 608 | function onDrop(source, target) { 609 | undo_stack = []; 610 | removeGreySquares(); 611 | 612 | // see if the move is legal 613 | var move = game.move({ 614 | from: source, 615 | to: target, 616 | promotion: 'q', // NOTE: always promote to a queen for example simplicity 617 | }); 618 | 619 | // Illegal move 620 | if (move === null) return 'snapback'; 621 | 622 | globalSum = evaluateBoard(game, move, globalSum, 'b'); 623 | updateAdvantage(); 624 | 625 | // Highlight latest move 626 | $board.find('.' + squareClass).removeClass('highlight-white'); 627 | 628 | $board.find('.square-' + move.from).addClass('highlight-white'); 629 | squareToHighlight = move.to; 630 | colorToHighlight = 'white'; 631 | 632 | $board 633 | .find('.square-' + squareToHighlight) 634 | .addClass('highlight-' + colorToHighlight); 635 | 636 | if (!checkStatus('black')); 637 | { 638 | // Make the best move for black 639 | window.setTimeout(function () { 640 | makeBestMove('b'); 641 | window.setTimeout(function () { 642 | showHint(); 643 | }, 250); 644 | }, 250); 645 | } 646 | } 647 | 648 | function onMouseoverSquare(square, piece) { 649 | // get list of possible moves for this square 650 | var moves = game.moves({ 651 | square: square, 652 | verbose: true, 653 | }); 654 | 655 | // exit if there are no moves available for this square 656 | if (moves.length === 0) return; 657 | 658 | // highlight the square they moused over 659 | greySquare(square); 660 | 661 | // highlight the possible squares for this piece 662 | for (var i = 0; i < moves.length; i++) { 663 | greySquare(moves[i].to); 664 | } 665 | } 666 | 667 | function onMouseoutSquare(square, piece) { 668 | removeGreySquares(); 669 | } 670 | 671 | function onSnapEnd() { 672 | board.position(game.fen()); 673 | } 674 | -------------------------------------------------------------------------------- /js/chess.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Jeff Hlywa (jhlywa@gmail.com) 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, 11 | * this list of conditions and the following disclaimer in the documentation 12 | * and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | * POSSIBILITY OF SUCH DAMAGE. 25 | * 26 | *----------------------------------------------------------------------------*/ 27 | 28 | var Chess = function(fen) { 29 | var BLACK = 'b' 30 | var WHITE = 'w' 31 | 32 | var EMPTY = -1 33 | 34 | var PAWN = 'p' 35 | var KNIGHT = 'n' 36 | var BISHOP = 'b' 37 | var ROOK = 'r' 38 | var QUEEN = 'q' 39 | var KING = 'k' 40 | 41 | var SYMBOLS = 'pnbrqkPNBRQK' 42 | 43 | var DEFAULT_POSITION = 44 | 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' 45 | 46 | var POSSIBLE_RESULTS = ['1-0', '0-1', '1/2-1/2', '*'] 47 | 48 | var PAWN_OFFSETS = { 49 | b: [16, 32, 17, 15], 50 | w: [-16, -32, -17, -15] 51 | } 52 | 53 | var PIECE_OFFSETS = { 54 | n: [-18, -33, -31, -14, 18, 33, 31, 14], 55 | b: [-17, -15, 17, 15], 56 | r: [-16, 1, 16, -1], 57 | q: [-17, -16, -15, 1, 17, 16, 15, -1], 58 | k: [-17, -16, -15, 1, 17, 16, 15, -1] 59 | } 60 | 61 | // prettier-ignore 62 | var ATTACKS = [ 63 | 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20, 0, 64 | 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0, 65 | 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0, 66 | 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0, 67 | 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0, 68 | 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0, 69 | 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0, 70 | 24,24,24,24,24,24,56, 0, 56,24,24,24,24,24,24, 0, 71 | 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0, 72 | 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0, 73 | 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0, 74 | 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0, 75 | 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0, 76 | 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0, 77 | 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20 78 | ]; 79 | 80 | // prettier-ignore 81 | var RAYS = [ 82 | 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, 83 | 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, 84 | 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0, 85 | 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, 86 | 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, 87 | 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0, 88 | 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0, 89 | 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1,-1, -1, -1, -1, 0, 90 | 0, 0, 0, 0, 0, 0,-15,-16,-17, 0, 0, 0, 0, 0, 0, 0, 91 | 0, 0, 0, 0, 0,-15, 0,-16, 0,-17, 0, 0, 0, 0, 0, 0, 92 | 0, 0, 0, 0,-15, 0, 0,-16, 0, 0,-17, 0, 0, 0, 0, 0, 93 | 0, 0, 0,-15, 0, 0, 0,-16, 0, 0, 0,-17, 0, 0, 0, 0, 94 | 0, 0,-15, 0, 0, 0, 0,-16, 0, 0, 0, 0,-17, 0, 0, 0, 95 | 0,-15, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0,-17, 0, 0, 96 | -15, 0, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0, 0,-17 97 | ]; 98 | 99 | var SHIFTS = { p: 0, n: 1, b: 2, r: 3, q: 4, k: 5 } 100 | 101 | var FLAGS = { 102 | NORMAL: 'n', 103 | CAPTURE: 'c', 104 | BIG_PAWN: 'b', 105 | EP_CAPTURE: 'e', 106 | PROMOTION: 'p', 107 | KSIDE_CASTLE: 'k', 108 | QSIDE_CASTLE: 'q' 109 | } 110 | 111 | var BITS = { 112 | NORMAL: 1, 113 | CAPTURE: 2, 114 | BIG_PAWN: 4, 115 | EP_CAPTURE: 8, 116 | PROMOTION: 16, 117 | KSIDE_CASTLE: 32, 118 | QSIDE_CASTLE: 64 119 | } 120 | 121 | var RANK_1 = 7 122 | var RANK_2 = 6 123 | var RANK_3 = 5 124 | var RANK_4 = 4 125 | var RANK_5 = 3 126 | var RANK_6 = 2 127 | var RANK_7 = 1 128 | var RANK_8 = 0 129 | 130 | // prettier-ignore 131 | var SQUARES = { 132 | a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7, 133 | a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23, 134 | a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39, 135 | a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55, 136 | a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71, 137 | a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87, 138 | a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103, 139 | a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119 140 | }; 141 | 142 | var ROOKS = { 143 | w: [ 144 | { square: SQUARES.a1, flag: BITS.QSIDE_CASTLE }, 145 | { square: SQUARES.h1, flag: BITS.KSIDE_CASTLE } 146 | ], 147 | b: [ 148 | { square: SQUARES.a8, flag: BITS.QSIDE_CASTLE }, 149 | { square: SQUARES.h8, flag: BITS.KSIDE_CASTLE } 150 | ] 151 | } 152 | 153 | var board = new Array(128) 154 | var kings = { w: EMPTY, b: EMPTY } 155 | var turn = WHITE 156 | var castling = { w: 0, b: 0 } 157 | var ep_square = EMPTY 158 | var half_moves = 0 159 | var move_number = 1 160 | var history = [] 161 | var header = {} 162 | var comments = {} 163 | 164 | /* if the user passes in a fen string, load it, else default to 165 | * starting position 166 | */ 167 | if (typeof fen === 'undefined') { 168 | load(DEFAULT_POSITION) 169 | } else { 170 | load(fen) 171 | } 172 | 173 | function clear(keep_headers) { 174 | if (typeof keep_headers === 'undefined') { 175 | keep_headers = false 176 | } 177 | 178 | board = new Array(128) 179 | kings = { w: EMPTY, b: EMPTY } 180 | turn = WHITE 181 | castling = { w: 0, b: 0 } 182 | ep_square = EMPTY 183 | half_moves = 0 184 | move_number = 1 185 | history = [] 186 | if (!keep_headers) header = {} 187 | comments = {} 188 | update_setup(generate_fen()) 189 | } 190 | 191 | function prune_comments() { 192 | var reversed_history = []; 193 | var current_comments = {}; 194 | var copy_comment = function(fen) { 195 | if (fen in comments) { 196 | current_comments[fen] = comments[fen]; 197 | } 198 | }; 199 | while (history.length > 0) { 200 | reversed_history.push(undo_move()); 201 | } 202 | copy_comment(generate_fen()); 203 | while (reversed_history.length > 0) { 204 | make_move(reversed_history.pop()); 205 | copy_comment(generate_fen()); 206 | } 207 | comments = current_comments; 208 | } 209 | 210 | function reset() { 211 | load(DEFAULT_POSITION) 212 | } 213 | 214 | function load(fen, keep_headers) { 215 | if (typeof keep_headers === 'undefined') { 216 | keep_headers = false 217 | } 218 | 219 | var tokens = fen.split(/\s+/) 220 | var position = tokens[0] 221 | var square = 0 222 | 223 | if (!validate_fen(fen).valid) { 224 | return false 225 | } 226 | 227 | clear(keep_headers) 228 | 229 | for (var i = 0; i < position.length; i++) { 230 | var piece = position.charAt(i) 231 | 232 | if (piece === '/') { 233 | square += 8 234 | } else if (is_digit(piece)) { 235 | square += parseInt(piece, 10) 236 | } else { 237 | var color = piece < 'a' ? WHITE : BLACK 238 | put({ type: piece.toLowerCase(), color: color }, algebraic(square)) 239 | square++ 240 | } 241 | } 242 | 243 | turn = tokens[1] 244 | 245 | if (tokens[2].indexOf('K') > -1) { 246 | castling.w |= BITS.KSIDE_CASTLE 247 | } 248 | if (tokens[2].indexOf('Q') > -1) { 249 | castling.w |= BITS.QSIDE_CASTLE 250 | } 251 | if (tokens[2].indexOf('k') > -1) { 252 | castling.b |= BITS.KSIDE_CASTLE 253 | } 254 | if (tokens[2].indexOf('q') > -1) { 255 | castling.b |= BITS.QSIDE_CASTLE 256 | } 257 | 258 | ep_square = tokens[3] === '-' ? EMPTY : SQUARES[tokens[3]] 259 | half_moves = parseInt(tokens[4], 10) 260 | move_number = parseInt(tokens[5], 10) 261 | 262 | update_setup(generate_fen()) 263 | 264 | return true 265 | } 266 | 267 | /* TODO: this function is pretty much crap - it validates structure but 268 | * completely ignores content (e.g. doesn't verify that each side has a king) 269 | * ... we should rewrite this, and ditch the silly error_number field while 270 | * we're at it 271 | */ 272 | function validate_fen(fen) { 273 | var errors = { 274 | 0: 'No errors.', 275 | 1: 'FEN string must contain six space-delimited fields.', 276 | 2: '6th field (move number) must be a positive integer.', 277 | 3: '5th field (half move counter) must be a non-negative integer.', 278 | 4: '4th field (en-passant square) is invalid.', 279 | 5: '3rd field (castling availability) is invalid.', 280 | 6: '2nd field (side to move) is invalid.', 281 | 7: "1st field (piece positions) does not contain 8 '/'-delimited rows.", 282 | 8: '1st field (piece positions) is invalid [consecutive numbers].', 283 | 9: '1st field (piece positions) is invalid [invalid piece].', 284 | 10: '1st field (piece positions) is invalid [row too large].', 285 | 11: 'Illegal en-passant square' 286 | } 287 | 288 | /* 1st criterion: 6 space-seperated fields? */ 289 | var tokens = fen.split(/\s+/) 290 | if (tokens.length !== 6) { 291 | return { valid: false, error_number: 1, error: errors[1] } 292 | } 293 | 294 | /* 2nd criterion: move number field is a integer value > 0? */ 295 | if (isNaN(tokens[5]) || parseInt(tokens[5], 10) <= 0) { 296 | return { valid: false, error_number: 2, error: errors[2] } 297 | } 298 | 299 | /* 3rd criterion: half move counter is an integer >= 0? */ 300 | if (isNaN(tokens[4]) || parseInt(tokens[4], 10) < 0) { 301 | return { valid: false, error_number: 3, error: errors[3] } 302 | } 303 | 304 | /* 4th criterion: 4th field is a valid e.p.-string? */ 305 | if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) { 306 | return { valid: false, error_number: 4, error: errors[4] } 307 | } 308 | 309 | /* 5th criterion: 3th field is a valid castle-string? */ 310 | if (!/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(tokens[2])) { 311 | return { valid: false, error_number: 5, error: errors[5] } 312 | } 313 | 314 | /* 6th criterion: 2nd field is "w" (white) or "b" (black)? */ 315 | if (!/^(w|b)$/.test(tokens[1])) { 316 | return { valid: false, error_number: 6, error: errors[6] } 317 | } 318 | 319 | /* 7th criterion: 1st field contains 8 rows? */ 320 | var rows = tokens[0].split('/') 321 | if (rows.length !== 8) { 322 | return { valid: false, error_number: 7, error: errors[7] } 323 | } 324 | 325 | /* 8th criterion: every row is valid? */ 326 | for (var i = 0; i < rows.length; i++) { 327 | /* check for right sum of fields AND not two numbers in succession */ 328 | var sum_fields = 0 329 | var previous_was_number = false 330 | 331 | for (var k = 0; k < rows[i].length; k++) { 332 | if (!isNaN(rows[i][k])) { 333 | if (previous_was_number) { 334 | return { valid: false, error_number: 8, error: errors[8] } 335 | } 336 | sum_fields += parseInt(rows[i][k], 10) 337 | previous_was_number = true 338 | } else { 339 | if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) { 340 | return { valid: false, error_number: 9, error: errors[9] } 341 | } 342 | sum_fields += 1 343 | previous_was_number = false 344 | } 345 | } 346 | if (sum_fields !== 8) { 347 | return { valid: false, error_number: 10, error: errors[10] } 348 | } 349 | } 350 | 351 | if ( 352 | (tokens[3][1] == '3' && tokens[1] == 'w') || 353 | (tokens[3][1] == '6' && tokens[1] == 'b') 354 | ) { 355 | return { valid: false, error_number: 11, error: errors[11] } 356 | } 357 | 358 | /* everything's okay! */ 359 | return { valid: true, error_number: 0, error: errors[0] } 360 | } 361 | 362 | function generate_fen() { 363 | var empty = 0 364 | var fen = '' 365 | 366 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 367 | if (board[i] == null) { 368 | empty++ 369 | } else { 370 | if (empty > 0) { 371 | fen += empty 372 | empty = 0 373 | } 374 | var color = board[i].color 375 | var piece = board[i].type 376 | 377 | fen += color === WHITE ? piece.toUpperCase() : piece.toLowerCase() 378 | } 379 | 380 | if ((i + 1) & 0x88) { 381 | if (empty > 0) { 382 | fen += empty 383 | } 384 | 385 | if (i !== SQUARES.h1) { 386 | fen += '/' 387 | } 388 | 389 | empty = 0 390 | i += 8 391 | } 392 | } 393 | 394 | var cflags = '' 395 | if (castling[WHITE] & BITS.KSIDE_CASTLE) { 396 | cflags += 'K' 397 | } 398 | if (castling[WHITE] & BITS.QSIDE_CASTLE) { 399 | cflags += 'Q' 400 | } 401 | if (castling[BLACK] & BITS.KSIDE_CASTLE) { 402 | cflags += 'k' 403 | } 404 | if (castling[BLACK] & BITS.QSIDE_CASTLE) { 405 | cflags += 'q' 406 | } 407 | 408 | /* do we have an empty castling flag? */ 409 | cflags = cflags || '-' 410 | var epflags = ep_square === EMPTY ? '-' : algebraic(ep_square) 411 | 412 | return [fen, turn, cflags, epflags, half_moves, move_number].join(' ') 413 | } 414 | 415 | function set_header(args) { 416 | for (var i = 0; i < args.length; i += 2) { 417 | if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') { 418 | header[args[i]] = args[i + 1] 419 | } 420 | } 421 | return header 422 | } 423 | 424 | /* called when the initial board setup is changed with put() or remove(). 425 | * modifies the SetUp and FEN properties of the header object. if the FEN is 426 | * equal to the default position, the SetUp and FEN are deleted 427 | * the setup is only updated if history.length is zero, ie moves haven't been 428 | * made. 429 | */ 430 | function update_setup(fen) { 431 | if (history.length > 0) return 432 | 433 | if (fen !== DEFAULT_POSITION) { 434 | header['SetUp'] = '1' 435 | header['FEN'] = fen 436 | } else { 437 | delete header['SetUp'] 438 | delete header['FEN'] 439 | } 440 | } 441 | 442 | function get(square) { 443 | var piece = board[SQUARES[square]] 444 | return piece ? { type: piece.type, color: piece.color } : null 445 | } 446 | 447 | function put(piece, square) { 448 | /* check for valid piece object */ 449 | if (!('type' in piece && 'color' in piece)) { 450 | return false 451 | } 452 | 453 | /* check for piece */ 454 | if (SYMBOLS.indexOf(piece.type.toLowerCase()) === -1) { 455 | return false 456 | } 457 | 458 | /* check for valid square */ 459 | if (!(square in SQUARES)) { 460 | return false 461 | } 462 | 463 | var sq = SQUARES[square] 464 | 465 | /* don't let the user place more than one king */ 466 | if ( 467 | piece.type == KING && 468 | !(kings[piece.color] == EMPTY || kings[piece.color] == sq) 469 | ) { 470 | return false 471 | } 472 | 473 | board[sq] = { type: piece.type, color: piece.color } 474 | if (piece.type === KING) { 475 | kings[piece.color] = sq 476 | } 477 | 478 | update_setup(generate_fen()) 479 | 480 | return true 481 | } 482 | 483 | function remove(square) { 484 | var piece = get(square) 485 | board[SQUARES[square]] = null 486 | if (piece && piece.type === KING) { 487 | kings[piece.color] = EMPTY 488 | } 489 | 490 | update_setup(generate_fen()) 491 | 492 | return piece 493 | } 494 | 495 | function build_move(board, from, to, flags, promotion) { 496 | var move = { 497 | color: turn, 498 | from: from, 499 | to: to, 500 | flags: flags, 501 | piece: board[from].type 502 | } 503 | 504 | if (promotion) { 505 | move.flags |= BITS.PROMOTION 506 | move.promotion = promotion 507 | } 508 | 509 | if (board[to]) { 510 | move.captured = board[to].type 511 | } else if (flags & BITS.EP_CAPTURE) { 512 | move.captured = PAWN 513 | } 514 | return move 515 | } 516 | 517 | function generate_moves(options) { 518 | function add_move(board, moves, from, to, flags) { 519 | /* if pawn promotion */ 520 | if ( 521 | board[from].type === PAWN && 522 | (rank(to) === RANK_8 || rank(to) === RANK_1) 523 | ) { 524 | var pieces = [QUEEN, ROOK, BISHOP, KNIGHT] 525 | for (var i = 0, len = pieces.length; i < len; i++) { 526 | moves.push(build_move(board, from, to, flags, pieces[i])) 527 | } 528 | } else { 529 | moves.push(build_move(board, from, to, flags)) 530 | } 531 | } 532 | 533 | var moves = [] 534 | var us = turn 535 | var them = swap_color(us) 536 | var second_rank = { b: RANK_7, w: RANK_2 } 537 | 538 | var first_sq = SQUARES.a8 539 | var last_sq = SQUARES.h1 540 | var single_square = false 541 | 542 | /* do we want legal moves? */ 543 | var legal = 544 | typeof options !== 'undefined' && 'legal' in options 545 | ? options.legal 546 | : true 547 | 548 | /* are we generating moves for a single square? */ 549 | if (typeof options !== 'undefined' && 'square' in options) { 550 | if (options.square in SQUARES) { 551 | first_sq = last_sq = SQUARES[options.square] 552 | single_square = true 553 | } else { 554 | /* invalid square */ 555 | return [] 556 | } 557 | } 558 | 559 | for (var i = first_sq; i <= last_sq; i++) { 560 | /* did we run off the end of the board */ 561 | if (i & 0x88) { 562 | i += 7 563 | continue 564 | } 565 | 566 | var piece = board[i] 567 | if (piece == null || piece.color !== us) { 568 | continue 569 | } 570 | 571 | if (piece.type === PAWN) { 572 | /* single square, non-capturing */ 573 | var square = i + PAWN_OFFSETS[us][0] 574 | if (board[square] == null) { 575 | add_move(board, moves, i, square, BITS.NORMAL) 576 | 577 | /* double square */ 578 | var square = i + PAWN_OFFSETS[us][1] 579 | if (second_rank[us] === rank(i) && board[square] == null) { 580 | add_move(board, moves, i, square, BITS.BIG_PAWN) 581 | } 582 | } 583 | 584 | /* pawn captures */ 585 | for (j = 2; j < 4; j++) { 586 | var square = i + PAWN_OFFSETS[us][j] 587 | if (square & 0x88) continue 588 | 589 | if (board[square] != null && board[square].color === them) { 590 | add_move(board, moves, i, square, BITS.CAPTURE) 591 | } else if (square === ep_square) { 592 | add_move(board, moves, i, ep_square, BITS.EP_CAPTURE) 593 | } 594 | } 595 | } else { 596 | for (var j = 0, len = PIECE_OFFSETS[piece.type].length; j < len; j++) { 597 | var offset = PIECE_OFFSETS[piece.type][j] 598 | var square = i 599 | 600 | while (true) { 601 | square += offset 602 | if (square & 0x88) break 603 | 604 | if (board[square] == null) { 605 | add_move(board, moves, i, square, BITS.NORMAL) 606 | } else { 607 | if (board[square].color === us) break 608 | add_move(board, moves, i, square, BITS.CAPTURE) 609 | break 610 | } 611 | 612 | /* break, if knight or king */ 613 | if (piece.type === 'n' || piece.type === 'k') break 614 | } 615 | } 616 | } 617 | } 618 | 619 | /* check for castling if: a) we're generating all moves, or b) we're doing 620 | * single square move generation on the king's square 621 | */ 622 | if (!single_square || last_sq === kings[us]) { 623 | /* king-side castling */ 624 | if (castling[us] & BITS.KSIDE_CASTLE) { 625 | var castling_from = kings[us] 626 | var castling_to = castling_from + 2 627 | 628 | if ( 629 | board[castling_from + 1] == null && 630 | board[castling_to] == null && 631 | !attacked(them, kings[us]) && 632 | !attacked(them, castling_from + 1) && 633 | !attacked(them, castling_to) 634 | ) { 635 | add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE) 636 | } 637 | } 638 | 639 | /* queen-side castling */ 640 | if (castling[us] & BITS.QSIDE_CASTLE) { 641 | var castling_from = kings[us] 642 | var castling_to = castling_from - 2 643 | 644 | if ( 645 | board[castling_from - 1] == null && 646 | board[castling_from - 2] == null && 647 | board[castling_from - 3] == null && 648 | !attacked(them, kings[us]) && 649 | !attacked(them, castling_from - 1) && 650 | !attacked(them, castling_to) 651 | ) { 652 | add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE) 653 | } 654 | } 655 | } 656 | 657 | /* return all pseudo-legal moves (this includes moves that allow the king 658 | * to be captured) 659 | */ 660 | if (!legal) { 661 | return moves 662 | } 663 | 664 | /* filter out illegal moves */ 665 | var legal_moves = [] 666 | for (var i = 0, len = moves.length; i < len; i++) { 667 | make_move(moves[i]) 668 | if (!king_attacked(us)) { 669 | legal_moves.push(moves[i]) 670 | } 671 | undo_move() 672 | } 673 | 674 | return legal_moves 675 | } 676 | 677 | /* convert a move from 0x88 coordinates to Standard Algebraic Notation 678 | * (SAN) 679 | * 680 | * @param {boolean} sloppy Use the sloppy SAN generator to work around over 681 | * disambiguation bugs in Fritz and Chessbase. See below: 682 | * 683 | * r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4 684 | * 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned 685 | * 4. ... Ne7 is technically the valid SAN 686 | */ 687 | function move_to_san(move, sloppy) { 688 | var output = '' 689 | 690 | if (move.flags & BITS.KSIDE_CASTLE) { 691 | output = 'O-O' 692 | } else if (move.flags & BITS.QSIDE_CASTLE) { 693 | output = 'O-O-O' 694 | } else { 695 | var disambiguator = get_disambiguator(move, sloppy) 696 | 697 | if (move.piece !== PAWN) { 698 | output += move.piece.toUpperCase() + disambiguator 699 | } 700 | 701 | if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) { 702 | if (move.piece === PAWN) { 703 | output += algebraic(move.from)[0] 704 | } 705 | output += 'x' 706 | } 707 | 708 | output += algebraic(move.to) 709 | 710 | if (move.flags & BITS.PROMOTION) { 711 | output += '=' + move.promotion.toUpperCase() 712 | } 713 | } 714 | 715 | make_move(move) 716 | if (in_check()) { 717 | if (in_checkmate()) { 718 | output += '#' 719 | } else { 720 | output += '+' 721 | } 722 | } 723 | undo_move() 724 | 725 | return output 726 | } 727 | 728 | // parses all of the decorators out of a SAN string 729 | function stripped_san(move) { 730 | return move.replace(/=/, '').replace(/[+#]?[?!]*$/, '') 731 | } 732 | 733 | function attacked(color, square) { 734 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 735 | /* did we run off the end of the board */ 736 | if (i & 0x88) { 737 | i += 7 738 | continue 739 | } 740 | 741 | /* if empty square or wrong color */ 742 | if (board[i] == null || board[i].color !== color) continue 743 | 744 | var piece = board[i] 745 | var difference = i - square 746 | var index = difference + 119 747 | 748 | if (ATTACKS[index] & (1 << SHIFTS[piece.type])) { 749 | if (piece.type === PAWN) { 750 | if (difference > 0) { 751 | if (piece.color === WHITE) return true 752 | } else { 753 | if (piece.color === BLACK) return true 754 | } 755 | continue 756 | } 757 | 758 | /* if the piece is a knight or a king */ 759 | if (piece.type === 'n' || piece.type === 'k') return true 760 | 761 | var offset = RAYS[index] 762 | var j = i + offset 763 | 764 | var blocked = false 765 | while (j !== square) { 766 | if (board[j] != null) { 767 | blocked = true 768 | break 769 | } 770 | j += offset 771 | } 772 | 773 | if (!blocked) return true 774 | } 775 | } 776 | 777 | return false 778 | } 779 | 780 | function king_attacked(color) { 781 | return attacked(swap_color(color), kings[color]) 782 | } 783 | 784 | function in_check() { 785 | return king_attacked(turn) 786 | } 787 | 788 | function in_checkmate() { 789 | return in_check() && generate_moves().length === 0 790 | } 791 | 792 | function in_stalemate() { 793 | return !in_check() && generate_moves().length === 0 794 | } 795 | 796 | function insufficient_material() { 797 | var pieces = {} 798 | var bishops = [] 799 | var num_pieces = 0 800 | var sq_color = 0 801 | 802 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 803 | sq_color = (sq_color + 1) % 2 804 | if (i & 0x88) { 805 | i += 7 806 | continue 807 | } 808 | 809 | var piece = board[i] 810 | if (piece) { 811 | pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1 812 | if (piece.type === BISHOP) { 813 | bishops.push(sq_color) 814 | } 815 | num_pieces++ 816 | } 817 | } 818 | 819 | /* k vs. k */ 820 | if (num_pieces === 2) { 821 | return true 822 | } else if ( 823 | /* k vs. kn .... or .... k vs. kb */ 824 | num_pieces === 3 && 825 | (pieces[BISHOP] === 1 || pieces[KNIGHT] === 1) 826 | ) { 827 | return true 828 | } else if (num_pieces === pieces[BISHOP] + 2) { 829 | /* kb vs. kb where any number of bishops are all on the same color */ 830 | var sum = 0 831 | var len = bishops.length 832 | for (var i = 0; i < len; i++) { 833 | sum += bishops[i] 834 | } 835 | if (sum === 0 || sum === len) { 836 | return true 837 | } 838 | } 839 | 840 | return false 841 | } 842 | 843 | function in_threefold_repetition() { 844 | /* TODO: while this function is fine for casual use, a better 845 | * implementation would use a Zobrist key (instead of FEN). the 846 | * Zobrist key would be maintained in the make_move/undo_move functions, 847 | * avoiding the costly that we do below. 848 | */ 849 | var moves = [] 850 | var positions = {} 851 | var repetition = false 852 | 853 | while (true) { 854 | var move = undo_move() 855 | if (!move) break 856 | moves.push(move) 857 | } 858 | 859 | while (true) { 860 | /* remove the last two fields in the FEN string, they're not needed 861 | * when checking for draw by rep */ 862 | var fen = generate_fen() 863 | .split(' ') 864 | .slice(0, 4) 865 | .join(' ') 866 | 867 | /* has the position occurred three or move times */ 868 | positions[fen] = fen in positions ? positions[fen] + 1 : 1 869 | if (positions[fen] >= 3) { 870 | repetition = true 871 | } 872 | 873 | if (!moves.length) { 874 | break 875 | } 876 | make_move(moves.pop()) 877 | } 878 | 879 | return repetition 880 | } 881 | 882 | function push(move) { 883 | history.push({ 884 | move: move, 885 | kings: { b: kings.b, w: kings.w }, 886 | turn: turn, 887 | castling: { b: castling.b, w: castling.w }, 888 | ep_square: ep_square, 889 | half_moves: half_moves, 890 | move_number: move_number 891 | }) 892 | } 893 | 894 | function make_move(move) { 895 | var us = turn 896 | var them = swap_color(us) 897 | push(move) 898 | 899 | board[move.to] = board[move.from] 900 | board[move.from] = null 901 | 902 | /* if ep capture, remove the captured pawn */ 903 | if (move.flags & BITS.EP_CAPTURE) { 904 | if (turn === BLACK) { 905 | board[move.to - 16] = null 906 | } else { 907 | board[move.to + 16] = null 908 | } 909 | } 910 | 911 | /* if pawn promotion, replace with new piece */ 912 | if (move.flags & BITS.PROMOTION) { 913 | board[move.to] = { type: move.promotion, color: us } 914 | } 915 | 916 | /* if we moved the king */ 917 | if (board[move.to].type === KING) { 918 | kings[board[move.to].color] = move.to 919 | 920 | /* if we castled, move the rook next to the king */ 921 | if (move.flags & BITS.KSIDE_CASTLE) { 922 | var castling_to = move.to - 1 923 | var castling_from = move.to + 1 924 | board[castling_to] = board[castling_from] 925 | board[castling_from] = null 926 | } else if (move.flags & BITS.QSIDE_CASTLE) { 927 | var castling_to = move.to + 1 928 | var castling_from = move.to - 2 929 | board[castling_to] = board[castling_from] 930 | board[castling_from] = null 931 | } 932 | 933 | /* turn off castling */ 934 | castling[us] = '' 935 | } 936 | 937 | /* turn off castling if we move a rook */ 938 | if (castling[us]) { 939 | for (var i = 0, len = ROOKS[us].length; i < len; i++) { 940 | if ( 941 | move.from === ROOKS[us][i].square && 942 | castling[us] & ROOKS[us][i].flag 943 | ) { 944 | castling[us] ^= ROOKS[us][i].flag 945 | break 946 | } 947 | } 948 | } 949 | 950 | /* turn off castling if we capture a rook */ 951 | if (castling[them]) { 952 | for (var i = 0, len = ROOKS[them].length; i < len; i++) { 953 | if ( 954 | move.to === ROOKS[them][i].square && 955 | castling[them] & ROOKS[them][i].flag 956 | ) { 957 | castling[them] ^= ROOKS[them][i].flag 958 | break 959 | } 960 | } 961 | } 962 | 963 | /* if big pawn move, update the en passant square */ 964 | if (move.flags & BITS.BIG_PAWN) { 965 | if (turn === 'b') { 966 | ep_square = move.to - 16 967 | } else { 968 | ep_square = move.to + 16 969 | } 970 | } else { 971 | ep_square = EMPTY 972 | } 973 | 974 | /* reset the 50 move counter if a pawn is moved or a piece is captured */ 975 | if (move.piece === PAWN) { 976 | half_moves = 0 977 | } else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) { 978 | half_moves = 0 979 | } else { 980 | half_moves++ 981 | } 982 | 983 | if (turn === BLACK) { 984 | move_number++ 985 | } 986 | turn = swap_color(turn) 987 | } 988 | 989 | function undo_move() { 990 | var old = history.pop() 991 | if (old == null) { 992 | return null 993 | } 994 | 995 | var move = old.move 996 | kings = old.kings 997 | turn = old.turn 998 | castling = old.castling 999 | ep_square = old.ep_square 1000 | half_moves = old.half_moves 1001 | move_number = old.move_number 1002 | 1003 | var us = turn 1004 | var them = swap_color(turn) 1005 | 1006 | board[move.from] = board[move.to] 1007 | board[move.from].type = move.piece // to undo any promotions 1008 | board[move.to] = null 1009 | 1010 | if (move.flags & BITS.CAPTURE) { 1011 | board[move.to] = { type: move.captured, color: them } 1012 | } else if (move.flags & BITS.EP_CAPTURE) { 1013 | var index 1014 | if (us === BLACK) { 1015 | index = move.to - 16 1016 | } else { 1017 | index = move.to + 16 1018 | } 1019 | board[index] = { type: PAWN, color: them } 1020 | } 1021 | 1022 | if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) { 1023 | var castling_to, castling_from 1024 | if (move.flags & BITS.KSIDE_CASTLE) { 1025 | castling_to = move.to + 1 1026 | castling_from = move.to - 1 1027 | } else if (move.flags & BITS.QSIDE_CASTLE) { 1028 | castling_to = move.to - 2 1029 | castling_from = move.to + 1 1030 | } 1031 | 1032 | board[castling_to] = board[castling_from] 1033 | board[castling_from] = null 1034 | } 1035 | 1036 | return move 1037 | } 1038 | 1039 | /* this function is used to uniquely identify ambiguous moves */ 1040 | function get_disambiguator(move, sloppy) { 1041 | var moves = generate_moves({ legal: !sloppy }) 1042 | 1043 | var from = move.from 1044 | var to = move.to 1045 | var piece = move.piece 1046 | 1047 | var ambiguities = 0 1048 | var same_rank = 0 1049 | var same_file = 0 1050 | 1051 | for (var i = 0, len = moves.length; i < len; i++) { 1052 | var ambig_from = moves[i].from 1053 | var ambig_to = moves[i].to 1054 | var ambig_piece = moves[i].piece 1055 | 1056 | /* if a move of the same piece type ends on the same to square, we'll 1057 | * need to add a disambiguator to the algebraic notation 1058 | */ 1059 | if (piece === ambig_piece && from !== ambig_from && to === ambig_to) { 1060 | ambiguities++ 1061 | 1062 | if (rank(from) === rank(ambig_from)) { 1063 | same_rank++ 1064 | } 1065 | 1066 | if (file(from) === file(ambig_from)) { 1067 | same_file++ 1068 | } 1069 | } 1070 | } 1071 | 1072 | if (ambiguities > 0) { 1073 | /* if there exists a similar moving piece on the same rank and file as 1074 | * the move in question, use the square as the disambiguator 1075 | */ 1076 | if (same_rank > 0 && same_file > 0) { 1077 | return algebraic(from) 1078 | } else if (same_file > 0) { 1079 | /* if the moving piece rests on the same file, use the rank symbol as the 1080 | * disambiguator 1081 | */ 1082 | return algebraic(from).charAt(1) 1083 | } else { 1084 | /* else use the file symbol */ 1085 | return algebraic(from).charAt(0) 1086 | } 1087 | } 1088 | 1089 | return '' 1090 | } 1091 | 1092 | function ascii() { 1093 | var s = ' +------------------------+\n' 1094 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 1095 | /* display the rank */ 1096 | if (file(i) === 0) { 1097 | s += ' ' + '87654321'[rank(i)] + ' |' 1098 | } 1099 | 1100 | /* empty piece */ 1101 | if (board[i] == null) { 1102 | s += ' . ' 1103 | } else { 1104 | var piece = board[i].type 1105 | var color = board[i].color 1106 | var symbol = color === WHITE ? piece.toUpperCase() : piece.toLowerCase() 1107 | s += ' ' + symbol + ' ' 1108 | } 1109 | 1110 | if ((i + 1) & 0x88) { 1111 | s += '|\n' 1112 | i += 8 1113 | } 1114 | } 1115 | s += ' +------------------------+\n' 1116 | s += ' a b c d e f g h\n' 1117 | 1118 | return s 1119 | } 1120 | 1121 | // convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates 1122 | function move_from_san(move, sloppy) { 1123 | // strip off any move decorations: e.g Nf3+?! 1124 | var clean_move = stripped_san(move) 1125 | 1126 | // if we're using the sloppy parser run a regex to grab piece, to, and from 1127 | // this should parse invalid SAN like: Pe2-e4, Rc1c4, Qf3xf7 1128 | if (sloppy) { 1129 | var matches = clean_move.match( 1130 | /([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/ 1131 | ) 1132 | if (matches) { 1133 | var piece = matches[1] 1134 | var from = matches[2] 1135 | var to = matches[3] 1136 | var promotion = matches[4] 1137 | } 1138 | } 1139 | 1140 | var moves = generate_moves() 1141 | for (var i = 0, len = moves.length; i < len; i++) { 1142 | // try the strict parser first, then the sloppy parser if requested 1143 | // by the user 1144 | if ( 1145 | clean_move === stripped_san(move_to_san(moves[i])) || 1146 | (sloppy && clean_move === stripped_san(move_to_san(moves[i], true))) 1147 | ) { 1148 | return moves[i] 1149 | } else { 1150 | if ( 1151 | matches && 1152 | (!piece || piece.toLowerCase() == moves[i].piece) && 1153 | SQUARES[from] == moves[i].from && 1154 | SQUARES[to] == moves[i].to && 1155 | (!promotion || promotion.toLowerCase() == moves[i].promotion) 1156 | ) { 1157 | return moves[i] 1158 | } 1159 | } 1160 | } 1161 | 1162 | return null 1163 | } 1164 | 1165 | /***************************************************************************** 1166 | * UTILITY FUNCTIONS 1167 | ****************************************************************************/ 1168 | function rank(i) { 1169 | return i >> 4 1170 | } 1171 | 1172 | function file(i) { 1173 | return i & 15 1174 | } 1175 | 1176 | function algebraic(i) { 1177 | var f = file(i), 1178 | r = rank(i) 1179 | return 'abcdefgh'.substring(f, f + 1) + '87654321'.substring(r, r + 1) 1180 | } 1181 | 1182 | function swap_color(c) { 1183 | return c === WHITE ? BLACK : WHITE 1184 | } 1185 | 1186 | function is_digit(c) { 1187 | return '0123456789'.indexOf(c) !== -1 1188 | } 1189 | 1190 | /* pretty = external move object */ 1191 | function make_pretty(ugly_move) { 1192 | var move = clone(ugly_move) 1193 | move.san = move_to_san(move, false) 1194 | move.to = algebraic(move.to) 1195 | move.from = algebraic(move.from) 1196 | 1197 | var flags = '' 1198 | 1199 | for (var flag in BITS) { 1200 | if (BITS[flag] & move.flags) { 1201 | flags += FLAGS[flag] 1202 | } 1203 | } 1204 | move.flags = flags 1205 | 1206 | return move 1207 | } 1208 | 1209 | function clone(obj) { 1210 | var dupe = obj instanceof Array ? [] : {} 1211 | 1212 | for (var property in obj) { 1213 | if (typeof property === 'object') { 1214 | dupe[property] = clone(obj[property]) 1215 | } else { 1216 | dupe[property] = obj[property] 1217 | } 1218 | } 1219 | 1220 | return dupe 1221 | } 1222 | 1223 | function trim(str) { 1224 | return str.replace(/^\s+|\s+$/g, '') 1225 | } 1226 | 1227 | /***************************************************************************** 1228 | * DEBUGGING UTILITIES 1229 | ****************************************************************************/ 1230 | function perft(depth) { 1231 | var moves = generate_moves({ legal: false }) 1232 | var nodes = 0 1233 | var color = turn 1234 | 1235 | for (var i = 0, len = moves.length; i < len; i++) { 1236 | make_move(moves[i]) 1237 | if (!king_attacked(color)) { 1238 | if (depth - 1 > 0) { 1239 | var child_nodes = perft(depth - 1) 1240 | nodes += child_nodes 1241 | } else { 1242 | nodes++ 1243 | } 1244 | } 1245 | undo_move() 1246 | } 1247 | 1248 | return nodes 1249 | } 1250 | 1251 | return { 1252 | /*************************************************************************** 1253 | * PUBLIC CONSTANTS (is there a better way to do this?) 1254 | **************************************************************************/ 1255 | WHITE: WHITE, 1256 | BLACK: BLACK, 1257 | PAWN: PAWN, 1258 | KNIGHT: KNIGHT, 1259 | BISHOP: BISHOP, 1260 | ROOK: ROOK, 1261 | QUEEN: QUEEN, 1262 | KING: KING, 1263 | SQUARES: (function() { 1264 | /* from the ECMA-262 spec (section 12.6.4): 1265 | * "The mechanics of enumerating the properties ... is 1266 | * implementation dependent" 1267 | * so: for (var sq in SQUARES) { keys.push(sq); } might not be 1268 | * ordered correctly 1269 | */ 1270 | var keys = [] 1271 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 1272 | if (i & 0x88) { 1273 | i += 7 1274 | continue 1275 | } 1276 | keys.push(algebraic(i)) 1277 | } 1278 | return keys 1279 | })(), 1280 | FLAGS: FLAGS, 1281 | 1282 | /*************************************************************************** 1283 | * PUBLIC API 1284 | **************************************************************************/ 1285 | load: function(fen) { 1286 | return load(fen) 1287 | }, 1288 | 1289 | reset: function() { 1290 | return reset() 1291 | }, 1292 | 1293 | moves: function(options) { 1294 | /* The internal representation of a chess move is in 0x88 format, and 1295 | * not meant to be human-readable. The code below converts the 0x88 1296 | * square coordinates to algebraic coordinates. It also prunes an 1297 | * unnecessary move keys resulting from a verbose call. 1298 | */ 1299 | 1300 | var ugly_moves = generate_moves(options) 1301 | var moves = [] 1302 | 1303 | for (var i = 0, len = ugly_moves.length; i < len; i++) { 1304 | /* does the user want a full move object (most likely not), or just 1305 | * SAN 1306 | */ 1307 | if ( 1308 | typeof options !== 'undefined' && 1309 | 'verbose' in options && 1310 | options.verbose 1311 | ) { 1312 | moves.push(make_pretty(ugly_moves[i])) 1313 | } else { 1314 | moves.push(move_to_san(ugly_moves[i], false)) 1315 | } 1316 | } 1317 | 1318 | return moves 1319 | }, 1320 | 1321 | ugly_moves: function(options) { 1322 | var ugly_moves = generate_moves(options); 1323 | return ugly_moves; 1324 | }, 1325 | 1326 | in_check: function() { 1327 | return in_check() 1328 | }, 1329 | 1330 | in_checkmate: function() { 1331 | return in_checkmate() 1332 | }, 1333 | 1334 | in_stalemate: function() { 1335 | return in_stalemate() 1336 | }, 1337 | 1338 | in_draw: function() { 1339 | return ( 1340 | half_moves >= 100 || 1341 | in_stalemate() || 1342 | insufficient_material() || 1343 | in_threefold_repetition() 1344 | ) 1345 | }, 1346 | 1347 | insufficient_material: function() { 1348 | return insufficient_material() 1349 | }, 1350 | 1351 | in_threefold_repetition: function() { 1352 | return in_threefold_repetition() 1353 | }, 1354 | 1355 | game_over: function() { 1356 | return ( 1357 | half_moves >= 100 || 1358 | in_checkmate() || 1359 | in_stalemate() || 1360 | insufficient_material() || 1361 | in_threefold_repetition() 1362 | ) 1363 | }, 1364 | 1365 | validate_fen: function(fen) { 1366 | return validate_fen(fen) 1367 | }, 1368 | 1369 | fen: function() { 1370 | return generate_fen() 1371 | }, 1372 | 1373 | board: function() { 1374 | var output = [], 1375 | row = [] 1376 | 1377 | for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { 1378 | if (board[i] == null) { 1379 | row.push(null) 1380 | } else { 1381 | row.push({ type: board[i].type, color: board[i].color }) 1382 | } 1383 | if ((i + 1) & 0x88) { 1384 | output.push(row) 1385 | row = [] 1386 | i += 8 1387 | } 1388 | } 1389 | 1390 | return output 1391 | }, 1392 | 1393 | pgn: function(options) { 1394 | /* using the specification from http://www.chessclub.com/help/PGN-spec 1395 | * example for html usage: .pgn({ max_width: 72, newline_char: "
" }) 1396 | */ 1397 | var newline = 1398 | typeof options === 'object' && typeof options.newline_char === 'string' 1399 | ? options.newline_char 1400 | : '\n' 1401 | var max_width = 1402 | typeof options === 'object' && typeof options.max_width === 'number' 1403 | ? options.max_width 1404 | : 0 1405 | var result = [] 1406 | var header_exists = false 1407 | 1408 | /* add the PGN header headerrmation */ 1409 | for (var i in header) { 1410 | /* TODO: order of enumerated properties in header object is not 1411 | * guaranteed, see ECMA-262 spec (section 12.6.4) 1412 | */ 1413 | result.push('[' + i + ' "' + header[i] + '"]' + newline) 1414 | header_exists = true 1415 | } 1416 | 1417 | if (header_exists && history.length) { 1418 | result.push(newline) 1419 | } 1420 | 1421 | var append_comment = function(move_string) { 1422 | var comment = comments[generate_fen()] 1423 | if (typeof comment !== 'undefined') { 1424 | var delimiter = move_string.length > 0 ? ' ' : ''; 1425 | move_string = `${move_string}${delimiter}{${comment}}` 1426 | } 1427 | return move_string 1428 | } 1429 | 1430 | /* pop all of history onto reversed_history */ 1431 | var reversed_history = [] 1432 | while (history.length > 0) { 1433 | reversed_history.push(undo_move()) 1434 | } 1435 | 1436 | var moves = [] 1437 | var move_string = '' 1438 | 1439 | /* special case of a commented starting position with no moves */ 1440 | if (reversed_history.length === 0) { 1441 | moves.push(append_comment('')) 1442 | } 1443 | 1444 | /* build the list of moves. a move_string looks like: "3. e3 e6" */ 1445 | while (reversed_history.length > 0) { 1446 | move_string = append_comment(move_string) 1447 | var move = reversed_history.pop() 1448 | 1449 | /* if the position started with black to move, start PGN with 1. ... */ 1450 | if (!history.length && move.color === 'b') { 1451 | move_string = move_number + '. ...' 1452 | } else if (move.color === 'w') { 1453 | /* store the previous generated move_string if we have one */ 1454 | if (move_string.length) { 1455 | moves.push(move_string) 1456 | } 1457 | move_string = move_number + '.' 1458 | } 1459 | 1460 | move_string = move_string + ' ' + move_to_san(move, false) 1461 | make_move(move) 1462 | } 1463 | 1464 | /* are there any other leftover moves? */ 1465 | if (move_string.length) { 1466 | moves.push(append_comment(move_string)) 1467 | } 1468 | 1469 | /* is there a result? */ 1470 | if (typeof header.Result !== 'undefined') { 1471 | moves.push(header.Result) 1472 | } 1473 | 1474 | /* history should be back to what it was before we started generating PGN, 1475 | * so join together moves 1476 | */ 1477 | if (max_width === 0) { 1478 | return result.join('') + moves.join(' ') 1479 | } 1480 | 1481 | var strip = function() { 1482 | if (result.length > 0 && result[result.length - 1] === ' ') { 1483 | result.pop(); 1484 | return true; 1485 | } 1486 | return false; 1487 | }; 1488 | 1489 | /* NB: this does not preserve comment whitespace. */ 1490 | var wrap_comment = function(width, move) { 1491 | for (var token of move.split(' ')) { 1492 | if (!token) { 1493 | continue; 1494 | } 1495 | if (width + token.length > max_width) { 1496 | while (strip()) { 1497 | width--; 1498 | } 1499 | result.push(newline); 1500 | width = 0; 1501 | } 1502 | result.push(token); 1503 | width += token.length; 1504 | result.push(' '); 1505 | width++; 1506 | } 1507 | if (strip()) { 1508 | width--; 1509 | } 1510 | return width; 1511 | }; 1512 | 1513 | /* wrap the PGN output at max_width */ 1514 | var current_width = 0 1515 | for (var i = 0; i < moves.length; i++) { 1516 | if (current_width + moves[i].length > max_width) { 1517 | if (moves[i].includes('{')) { 1518 | current_width = wrap_comment(current_width, moves[i]); 1519 | continue; 1520 | } 1521 | } 1522 | /* if the current move will push past max_width */ 1523 | if (current_width + moves[i].length > max_width && i !== 0) { 1524 | /* don't end the line with whitespace */ 1525 | if (result[result.length - 1] === ' ') { 1526 | result.pop() 1527 | } 1528 | 1529 | result.push(newline) 1530 | current_width = 0 1531 | } else if (i !== 0) { 1532 | result.push(' ') 1533 | current_width++ 1534 | } 1535 | result.push(moves[i]) 1536 | current_width += moves[i].length 1537 | } 1538 | 1539 | return result.join('') 1540 | }, 1541 | 1542 | load_pgn: function(pgn, options) { 1543 | // allow the user to specify the sloppy move parser to work around over 1544 | // disambiguation bugs in Fritz and Chessbase 1545 | var sloppy = 1546 | typeof options !== 'undefined' && 'sloppy' in options 1547 | ? options.sloppy 1548 | : false 1549 | 1550 | function mask(str) { 1551 | return str.replace(/\\/g, '\\') 1552 | } 1553 | 1554 | function has_keys(object) { 1555 | for (var key in object) { 1556 | return true 1557 | } 1558 | return false 1559 | } 1560 | 1561 | function parse_pgn_header(header, options) { 1562 | var newline_char = 1563 | typeof options === 'object' && 1564 | typeof options.newline_char === 'string' 1565 | ? options.newline_char 1566 | : '\r?\n' 1567 | var header_obj = {} 1568 | var headers = header.split(new RegExp(mask(newline_char))) 1569 | var key = '' 1570 | var value = '' 1571 | 1572 | for (var i = 0; i < headers.length; i++) { 1573 | key = headers[i].replace(/^\[([A-Z][A-Za-z]*)\s.*\]$/, '$1') 1574 | value = headers[i].replace(/^\[[A-Za-z]+\s"(.*)"\ *\]$/, '$1') 1575 | if (trim(key).length > 0) { 1576 | header_obj[key] = value 1577 | } 1578 | } 1579 | 1580 | return header_obj 1581 | } 1582 | 1583 | var newline_char = 1584 | typeof options === 'object' && typeof options.newline_char === 'string' 1585 | ? options.newline_char 1586 | : '\r?\n' 1587 | 1588 | // RegExp to split header. Takes advantage of the fact that header and movetext 1589 | // will always have a blank line between them (ie, two newline_char's). 1590 | // With default newline_char, will equal: /^(\[((?:\r?\n)|.)*\])(?:\r?\n){2}/ 1591 | var header_regex = new RegExp( 1592 | '^(\\[((?:' + 1593 | mask(newline_char) + 1594 | ')|.)*\\])' + 1595 | '(?:' + 1596 | mask(newline_char) + 1597 | '){2}' 1598 | ) 1599 | 1600 | // If no header given, begin with moves. 1601 | var header_string = header_regex.test(pgn) 1602 | ? header_regex.exec(pgn)[1] 1603 | : '' 1604 | 1605 | // Put the board in the starting position 1606 | reset() 1607 | 1608 | /* parse PGN header */ 1609 | var headers = parse_pgn_header(header_string, options) 1610 | for (var key in headers) { 1611 | set_header([key, headers[key]]) 1612 | } 1613 | 1614 | /* load the starting position indicated by [Setup '1'] and 1615 | * [FEN position] */ 1616 | if (headers['SetUp'] === '1') { 1617 | if (!('FEN' in headers && load(headers['FEN'], true))) { 1618 | // second argument to load: don't clear the headers 1619 | return false 1620 | } 1621 | } 1622 | 1623 | /* NB: the regexes below that delete move numbers, recursive 1624 | * annotations, and numeric annotation glyphs may also match 1625 | * text in comments. To prevent this, we transform comments 1626 | * by hex-encoding them in place and decoding them again after 1627 | * the other tokens have been deleted. 1628 | * 1629 | * While the spec states that PGN files should be ASCII encoded, 1630 | * we use {en,de}codeURIComponent here to support arbitrary UTF8 1631 | * as a convenience for modern users */ 1632 | 1633 | var to_hex = function(string) { 1634 | return Array 1635 | .from(string) 1636 | .map(function(c) { 1637 | /* encodeURI doesn't transform most ASCII characters, 1638 | * so we handle these ourselves */ 1639 | return c.charCodeAt(0) < 128 1640 | ? c.charCodeAt(0).toString(16) 1641 | : encodeURIComponent(c).replace(/\%/g, '').toLowerCase() 1642 | }) 1643 | .join('') 1644 | } 1645 | 1646 | var from_hex = function(string) { 1647 | return string.length == 0 1648 | ? '' 1649 | : decodeURIComponent('%' + string.match(/.{1,2}/g).join('%')) 1650 | } 1651 | 1652 | var encode_comment = function(string) { 1653 | string = string.replace(new RegExp(mask(newline_char), 'g'), ' ') 1654 | return `{${to_hex(string.slice(1, string.length - 1))}}` 1655 | } 1656 | 1657 | var decode_comment = function(string) { 1658 | if (string.startsWith('{') && string.endsWith('}')) { 1659 | return from_hex(string.slice(1, string.length - 1)) 1660 | } 1661 | } 1662 | 1663 | /* delete header to get the moves */ 1664 | var ms = pgn 1665 | .replace(header_string, '') 1666 | .replace( 1667 | /* encode comments so they don't get deleted below */ 1668 | new RegExp(`(\{[^}]*\})+?|;([^${mask(newline_char)}]*)`, 'g'), 1669 | function(match, bracket, semicolon) { 1670 | return bracket !== undefined 1671 | ? encode_comment(bracket) 1672 | : ' ' + encode_comment(`{${semicolon.slice(1)}}`) 1673 | } 1674 | ) 1675 | .replace(new RegExp(mask(newline_char), 'g'), ' ') 1676 | 1677 | /* delete recursive annotation variations */ 1678 | var rav_regex = /(\([^\(\)]+\))+?/g 1679 | while (rav_regex.test(ms)) { 1680 | ms = ms.replace(rav_regex, '') 1681 | } 1682 | 1683 | /* delete move numbers */ 1684 | ms = ms.replace(/\d+\.(\.\.)?/g, '') 1685 | 1686 | /* delete ... indicating black to move */ 1687 | ms = ms.replace(/\.\.\./g, '') 1688 | 1689 | /* delete numeric annotation glyphs */ 1690 | ms = ms.replace(/\$\d+/g, '') 1691 | 1692 | /* trim and get array of moves */ 1693 | var moves = trim(ms).split(new RegExp(/\s+/)) 1694 | 1695 | /* delete empty entries */ 1696 | moves = moves 1697 | .join(',') 1698 | .replace(/,,+/g, ',') 1699 | .split(',') 1700 | var move = '' 1701 | 1702 | for (var half_move = 0; half_move < moves.length - 1; half_move++) { 1703 | var comment = decode_comment(moves[half_move]) 1704 | if (comment !== undefined) { 1705 | comments[generate_fen()] = comment 1706 | continue 1707 | } 1708 | move = move_from_san(moves[half_move], sloppy) 1709 | 1710 | /* move not possible! (don't clear the board to examine to show the 1711 | * latest valid position) 1712 | */ 1713 | if (move == null) { 1714 | return false 1715 | } else { 1716 | make_move(move) 1717 | } 1718 | } 1719 | 1720 | comment = decode_comment(moves[moves.length - 1]) 1721 | if (comment !== undefined) { 1722 | comments[generate_fen()] = comment 1723 | moves.pop() 1724 | } 1725 | 1726 | /* examine last move */ 1727 | move = moves[moves.length - 1] 1728 | if (POSSIBLE_RESULTS.indexOf(move) > -1) { 1729 | if (has_keys(header) && typeof header.Result === 'undefined') { 1730 | set_header(['Result', move]) 1731 | } 1732 | } else { 1733 | move = move_from_san(move, sloppy) 1734 | if (move == null) { 1735 | return false 1736 | } else { 1737 | make_move(move) 1738 | } 1739 | } 1740 | return true 1741 | }, 1742 | 1743 | header: function() { 1744 | return set_header(arguments) 1745 | }, 1746 | 1747 | ascii: function() { 1748 | return ascii() 1749 | }, 1750 | 1751 | turn: function() { 1752 | return turn 1753 | }, 1754 | 1755 | move: function(move, options) { 1756 | /* The move function can be called with in the following parameters: 1757 | * 1758 | * .move('Nxb7') <- where 'move' is a case-sensitive SAN string 1759 | * 1760 | * .move({ from: 'h7', <- where the 'move' is a move object (additional 1761 | * to :'h8', fields are ignored) 1762 | * promotion: 'q', 1763 | * }) 1764 | */ 1765 | 1766 | // allow the user to specify the sloppy move parser to work around over 1767 | // disambiguation bugs in Fritz and Chessbase 1768 | var sloppy = 1769 | typeof options !== 'undefined' && 'sloppy' in options 1770 | ? options.sloppy 1771 | : false 1772 | 1773 | var move_obj = null 1774 | 1775 | if (typeof move === 'string') { 1776 | move_obj = move_from_san(move, sloppy) 1777 | } else if (typeof move === 'object') { 1778 | var moves = generate_moves() 1779 | 1780 | /* convert the pretty move object to an ugly move object */ 1781 | for (var i = 0, len = moves.length; i < len; i++) { 1782 | if ( 1783 | move.from === algebraic(moves[i].from) && 1784 | move.to === algebraic(moves[i].to) && 1785 | (!('promotion' in moves[i]) || 1786 | move.promotion === moves[i].promotion) 1787 | ) { 1788 | move_obj = moves[i] 1789 | break 1790 | } 1791 | } 1792 | } 1793 | 1794 | /* failed to find move */ 1795 | if (!move_obj) { 1796 | return null 1797 | } 1798 | 1799 | /* need to make a copy of move because we can't generate SAN after the 1800 | * move is made 1801 | */ 1802 | var pretty_move = make_pretty(move_obj) 1803 | 1804 | make_move(move_obj) 1805 | 1806 | return pretty_move 1807 | }, 1808 | 1809 | ugly_move: function(move_obj, options) { 1810 | var pretty_move = make_pretty(move_obj); 1811 | make_move(move_obj); 1812 | 1813 | return pretty_move; 1814 | }, 1815 | 1816 | undo: function() { 1817 | var move = undo_move() 1818 | return move ? make_pretty(move) : null 1819 | }, 1820 | 1821 | clear: function() { 1822 | return clear() 1823 | }, 1824 | 1825 | put: function(piece, square) { 1826 | return put(piece, square) 1827 | }, 1828 | 1829 | get: function(square) { 1830 | return get(square) 1831 | }, 1832 | 1833 | remove: function(square) { 1834 | return remove(square) 1835 | }, 1836 | 1837 | perft: function(depth) { 1838 | return perft(depth) 1839 | }, 1840 | 1841 | square_color: function(square) { 1842 | if (square in SQUARES) { 1843 | var sq_0x88 = SQUARES[square] 1844 | return (rank(sq_0x88) + file(sq_0x88)) % 2 === 0 ? 'light' : 'dark' 1845 | } 1846 | 1847 | return null 1848 | }, 1849 | 1850 | history: function(options) { 1851 | var reversed_history = [] 1852 | var move_history = [] 1853 | var verbose = 1854 | typeof options !== 'undefined' && 1855 | 'verbose' in options && 1856 | options.verbose 1857 | 1858 | while (history.length > 0) { 1859 | reversed_history.push(undo_move()) 1860 | } 1861 | 1862 | while (reversed_history.length > 0) { 1863 | var move = reversed_history.pop() 1864 | if (verbose) { 1865 | move_history.push(make_pretty(move)) 1866 | } else { 1867 | move_history.push(move_to_san(move)) 1868 | } 1869 | make_move(move) 1870 | } 1871 | 1872 | return move_history 1873 | }, 1874 | 1875 | get_comment: function() { 1876 | return comments[generate_fen()]; 1877 | }, 1878 | 1879 | set_comment: function(comment) { 1880 | comments[generate_fen()] = comment.replace('{', '[').replace('}', ']'); 1881 | }, 1882 | 1883 | delete_comment: function() { 1884 | var comment = comments[generate_fen()]; 1885 | delete comments[generate_fen()]; 1886 | return comment; 1887 | }, 1888 | 1889 | get_comments: function() { 1890 | prune_comments(); 1891 | return Object.keys(comments).map(function(fen) { 1892 | return {fen: fen, comment: comments[fen]}; 1893 | }); 1894 | }, 1895 | 1896 | delete_comments: function() { 1897 | prune_comments(); 1898 | return Object.keys(comments) 1899 | .map(function(fen) { 1900 | var comment = comments[fen]; 1901 | delete comments[fen]; 1902 | return {fen: fen, comment: comment}; 1903 | }); 1904 | } 1905 | } 1906 | } 1907 | 1908 | /* export Chess object if using node or any other CommonJS compatible 1909 | * environment */ 1910 | if (typeof exports !== 'undefined') exports.Chess = Chess 1911 | /* export Chess object for any RequireJS compatible environment */ 1912 | if (typeof define !== 'undefined') 1913 | define(function() { 1914 | return Chess 1915 | }) --------------------------------------------------------------------------------