forked from oakmac/chessboardjs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
315 lines (265 loc) · 8.29 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
'use strict';
let nQueens = (function () {
// Internal state and default values
let size = 4;
let counter = 0;
let board;
function start() {
// Start the game by resizing the board container element
resize();
// Clear the registry
updateRegistry();
// Cleanup existing board
if (board) {
board.destroy();
}
// Create a new board
board = ChessBoard('board', {
draggable: true,
dropOffBoard: 'trash',
sparePieces: true,
pieceTheme: 'website/img/chesspieces/wikipedia/{piece}.png',
size,
onDragStart,
onDragMove,
onDrop,
});
}
function updateRegistry(value) {
// Update the registry value and status message
if (value === undefined) {
counter = 0;
value = 0;
}
counter += value;
const n = (size - counter);
$('#status').text('Place ' + n + ' more Queen' + (n === 1 ? '' : 's') + ' on the board to win');
}
function base26Encode(column) {
// Encode a number to Base26 using the letters a-z
const a = 'a'.charCodeAt(0);
const j = column % 26;
const k = Math.floor(column / 26);
return (k ? String.fromCharCode(k - 1 + a) : '') + String.fromCharCode(j + a);
}
function base26Decode(str) {
// Decode a number from Base26 using the letters a-z
const base = 26;
const a = 'a'.charCodeAt(0);
let i = 0;
let n = 0;
for (const c of str.split('').reverse()) {
n += (c.charCodeAt(0) - a + 1) * Math.pow(base, i++);
}
return n - 1;
}
function parseSquare(square) {
// Given a location in the form "<base26_column><row>"
// Parse the string into column and row numbers
let matches = square.match(/^([a-z]+)([0-9]+)$/);
return {
column: base26Decode(matches[1]),
row: +matches[2] - 1,
};
}
function squareIsOccupied(square) {
// Return true if the location has a Queen
return $('[data-square="' + square + '"]').children('[data-piece]').length > 0;
}
function squareIsValid(square) {
// Return true if the location does not attack any Queens
return $('.hit').length == 0;
}
function clearAttacks(attacker) {
// Remove all "attack highlight" squares for the given attacker location
if ([ 'offboard', 'spare' ].indexOf(attacker) >= 0) {
return;
}
// Iterate each square that the attacker is covering
const $el = $('[data-attacked-by~="' + attacker + '"].attack');
$el.each(function () {
const data = ($(this).attr('data-attacked-by') || '').split(' ').filter((v) => v && v !== attacker);
$(this).attr('data-attacked-by', data.join(' '));
// Remove the highlight if this square is *only* attacked by the attacker
if (data.length === 0) {
$(this).removeClass('attack');
}
// Always remove the hit class
$(this).removeClass('hit');
});
}
function setAttacks(attacker, src) {
// Add "attack highlight" squares for the given attacker location
// src indicates the "current location" of the attacker as it moves
if ([ 'offboard', 'spare' ].indexOf(attacker) >= 0) {
return;
}
function setAttack(square) {
// Add the "attack highlight" to the given location
const $el = $('[data-square="' + square + '"]');
const data = ($el.attr('data-attacked-by') || '').split(' ');
// Add the attacker to the square's metadata iif it is not already recorded
if (data.indexOf(attacker) < 0) {
$el.attr('data-attacked-by', data.concat(attacker).join(' '));
}
// Add the "attack highlight"
$el.addClass('attack');
// If the location is occupied by a Queen, highlight it with an additional hit class
if (square !== src && squareIsOccupied(square)) {
$el.addClass('hit');
}
}
function setRow(row) {
// Add "attacker highlight" to an entire row
for (let i = 0; i < size; i++) {
const square = base26Encode(i) + (row + 1);
setAttack(square);
}
}
function setColumn(column) {
// Add "attacker highlight" to an entire column
for (let i = 0; i < size; i++) {
const square = base26Encode(column) + (i + 1);
setAttack(square);
}
}
// TODO: Simplify these four functions into one
function setDiagonalNW(square) {
// Add "attacker highlight" to the entire north-west diagonal
const source = parseSquare(square);
source.column--;
source.row--;
while (source.column >= 0 && source.row >= 0) {
const square = base26Encode(source.column) + (source.row + 1);
setAttack(square);
source.column--;
source.row--;
}
}
function setDiagonalNE(square) {
// Add "attacker highlight" to the entire north-east diagonal
const source = parseSquare(square);
source.column++;
source.row--;
while (source.column < size && source.row >= 0) {
const square = base26Encode(source.column) + (source.row + 1);
setAttack(square);
source.column++;
source.row--;
}
}
function setDiagonalSW(square) {
// Add "attacker highlight" to the entire south-west diagonal
const source = parseSquare(square);
source.column--;
source.row++;
while (source.column >= 0 && source.row < size) {
const square = base26Encode(source.column) + (source.row + 1);
setAttack(square);
source.column--;
source.row++;
}
}
function setDiagonalSE(square) {
// Add "attacker highlight" to the entire south-east diagonal
const source = parseSquare(square);
source.column++;
source.row++;
while (source.column < size && source.row < size) {
let square = base26Encode(source.column) + (source.row + 1);
setAttack(square);
source.column++;
source.row++;
}
}
// Highlight all squares that the attacker can reach
const source = parseSquare(attacker);
setColumn(source.column);
setRow(source.row);
setDiagonalNW(attacker);
setDiagonalNE(attacker);
setDiagonalSW(attacker);
setDiagonalSE(attacker);
}
function onDragStart(src) {
// Remove this Queen from the register
if (src !== 'spare') {
updateRegistry(-1);
}
}
function onDragMove(dst, lastPos, src) {
// Refresh the "attacker highlight" as a Queen moves
clearAttacks(lastPos);
// Special case for moving one Queen over the square occupied by another
if (lastPos !== src && squareIsOccupied(lastPos)) {
setAttacks(lastPos, lastPos);
}
// Add "attacker highlight" if the location is not currently occupied
if (dst == src || !squareIsOccupied(dst)) {
setAttacks(dst, src);
}
}
function onDrop(src, dst) {
// Special case for attempting to drop a Queen on a square occupied by another
if (squareIsOccupied(dst)) {
// Add the Queen back to the register if it hasn't moved
if (src === dst) {
updateRegistry(1);
}
// Move the Queen back to her original position
setAttacks(src, src);
return 'snapback';
}
// Special case for attempting an invalid move
if (!squareIsValid(dst)) {
clearAttacks(dst);
// Reset the Queen iif it was moved from an existing location on the board
if (src !== 'spare') {
updateRegistry(1);
setAttacks(src, src);
}
return 'snapback';
}
// The move was valid, add it to the registry and check the win condition
if (dst !== 'offboard') {
updateRegistry(1);
if (counter >= size) {
// A WINNER IS YOU!
next();
}
}
}
function restart() {
// Restart the game
size = 4;
start();
}
function next() {
// Advance to the next board
size++;
start();
}
function clear() {
// Remove all Queens from the board and reset the registry
board.clear();
$('.attack')
.removeClass('attack')
.removeClass('hit')
.attr('data-attacked-by', '');
updateRegistry();
}
function resize() {
// Resize the board container element
// XXX: chessboard.js does not play well with fractional square sizes
$('#board').css('width', (size * Math.max(50 - size, 25)) + 'px');
}
// Begin the game
start();
// Expose the public API
return {
start,
restart,
clear,
next,
};
})();