Skip to content

Instantly share code, notes, and snippets.

@Mon-Ouie
Last active December 22, 2021 14:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Mon-Ouie/e69a0cb2e2c8ed369b176483696e240e to your computer and use it in GitHub Desktop.
Save Mon-Ouie/e69a0cb2e2c8ed369b176483696e240e to your computer and use it in GitHub Desktop.
/*
* Copyright (c) 2021, Jeff Hlywa (jhlywa@gmail.com)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*----------------------------------------------------------------------------*/
var Chess = function(fen) {
var BLACK = 'b'
var WHITE = 'w'
var EMPTY = -1
var PAWN = 'p'
var KNIGHT = 'n'
var BISHOP = 'b'
var ROOK = 'r'
var QUEEN = 'q'
var KING = 'k'
var SYMBOLS = 'pnbrqkPNBRQK'
var DEFAULT_POSITION =
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
var POSSIBLE_RESULTS = ['1-0', '0-1', '1/2-1/2', '*']
var PAWN_OFFSETS = {
b: [16, 32, 17, 15],
w: [-16, -32, -17, -15]
}
var PIECE_OFFSETS = {
n: [-18, -33, -31, -14, 18, 33, 31, 14],
b: [-17, -15, 17, 15],
r: [-16, 1, 16, -1],
q: [-17, -16, -15, 1, 17, 16, 15, -1],
k: [-17, -16, -15, 1, 17, 16, 15, -1]
}
// prettier-ignore
var ATTACKS = [
20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20, 0,
0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0,
0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0,
0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0,
0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
24,24,24,24,24,24,56, 0, 56,24,24,24,24,24,24, 0,
0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0,
0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0,
0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0,
0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0,
20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20
];
// prettier-ignore
var RAYS = [
17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0,
0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0,
0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0,
0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0,
0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1,-1, -1, -1, -1, 0,
0, 0, 0, 0, 0, 0,-15,-16,-17, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0,-15, 0,-16, 0,-17, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,-15, 0, 0,-16, 0, 0,-17, 0, 0, 0, 0, 0,
0, 0, 0,-15, 0, 0, 0,-16, 0, 0, 0,-17, 0, 0, 0, 0,
0, 0,-15, 0, 0, 0, 0,-16, 0, 0, 0, 0,-17, 0, 0, 0,
0,-15, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0,-17, 0, 0,
-15, 0, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0, 0,-17
];
var SHIFTS = { p: 0, n: 1, b: 2, r: 3, q: 4, k: 5 }
var FLAGS = {
NORMAL: 'n',
CAPTURE: 'c',
BIG_PAWN: 'b',
EP_CAPTURE: 'e',
PROMOTION: 'p',
KSIDE_CASTLE: 'k',
QSIDE_CASTLE: 'q'
}
var BITS = {
NORMAL: 1,
CAPTURE: 2,
BIG_PAWN: 4,
EP_CAPTURE: 8,
PROMOTION: 16,
KSIDE_CASTLE: 32,
QSIDE_CASTLE: 64
}
var RANK_1 = 7
var RANK_2 = 6
var RANK_3 = 5
var RANK_4 = 4
var RANK_5 = 3
var RANK_6 = 2
var RANK_7 = 1
var RANK_8 = 0
// prettier-ignore
var SQUARES = {
a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7,
a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23,
a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39,
a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55,
a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71,
a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87,
a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103,
a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119
};
var ROOKS = {
w: [
{ square: SQUARES.a1, flag: BITS.QSIDE_CASTLE },
{ square: SQUARES.h1, flag: BITS.KSIDE_CASTLE }
],
b: [
{ square: SQUARES.a8, flag: BITS.QSIDE_CASTLE },
{ square: SQUARES.h8, flag: BITS.KSIDE_CASTLE }
]
}
var board = new Array(128)
var kings = { w: EMPTY, b: EMPTY }
var turn = WHITE
var castling = { w: 0, b: 0 }
var ep_square = EMPTY
var half_moves = 0
var move_number = 1
var history = []
var header = {}
var comments = {}
/* if the user passes in a fen string, load it, else default to
* starting position
*/
if (typeof fen === 'undefined') {
load(DEFAULT_POSITION)
} else {
load(fen)
}
function clear(keep_headers) {
if (typeof keep_headers === 'undefined') {
keep_headers = false
}
board = new Array(128)
kings = { w: EMPTY, b: EMPTY }
turn = WHITE
castling = { w: 0, b: 0 }
ep_square = EMPTY
half_moves = 0
move_number = 1
history = []
if (!keep_headers) header = {}
comments = {}
update_setup(generate_fen())
}
function prune_comments() {
var reversed_history = [];
var current_comments = {};
var copy_comment = function(fen) {
if (fen in comments) {
current_comments[fen] = comments[fen];
}
};
while (history.length > 0) {
reversed_history.push(undo_move());
}
copy_comment(generate_fen());
while (reversed_history.length > 0) {
make_move(reversed_history.pop());
copy_comment(generate_fen());
}
comments = current_comments;
}
function reset() {
load(DEFAULT_POSITION)
}
function load(fen, keep_headers) {
if (typeof keep_headers === 'undefined') {
keep_headers = false
}
var tokens = fen.split(/\s+/)
var position = tokens[0]
var square = 0
if (!validate_fen(fen).valid) {
return false
}
clear(keep_headers)
for (var i = 0; i < position.length; i++) {
var piece = position.charAt(i)
if (piece === '/') {
square += 8
} else if (is_digit(piece)) {
square += parseInt(piece, 10)
} else {
var color = piece < 'a' ? WHITE : BLACK
put({ type: piece.toLowerCase(), color: color }, algebraic(square))
square++
}
}
turn = tokens[1]
if (tokens[2].indexOf('K') > -1) {
castling.w |= BITS.KSIDE_CASTLE
}
if (tokens[2].indexOf('Q') > -1) {
castling.w |= BITS.QSIDE_CASTLE
}
if (tokens[2].indexOf('k') > -1) {
castling.b |= BITS.KSIDE_CASTLE
}
if (tokens[2].indexOf('q') > -1) {
castling.b |= BITS.QSIDE_CASTLE
}
ep_square = tokens[3] === '-' ? EMPTY : SQUARES[tokens[3]]
half_moves = parseInt(tokens[4], 10)
move_number = parseInt(tokens[5], 10)
update_setup(generate_fen())
return true
}
/* TODO: this function is pretty much crap - it validates structure but
* completely ignores content (e.g. doesn't verify that each side has a king)
* ... we should rewrite this, and ditch the silly error_number field while
* we're at it
*/
function validate_fen(fen) {
var errors = {
0: 'No errors.',
1: 'FEN string must contain six space-delimited fields.',
2: '6th field (move number) must be a positive integer.',
3: '5th field (half move counter) must be a non-negative integer.',
4: '4th field (en-passant square) is invalid.',
5: '3rd field (castling availability) is invalid.',
6: '2nd field (side to move) is invalid.',
7: "1st field (piece positions) does not contain 8 '/'-delimited rows.",
8: '1st field (piece positions) is invalid [consecutive numbers].',
9: '1st field (piece positions) is invalid [invalid piece].',
10: '1st field (piece positions) is invalid [row too large].',
11: 'Illegal en-passant square'
}
/* 1st criterion: 6 space-seperated fields? */
var tokens = fen.split(/\s+/)
if (tokens.length !== 6) {
return { valid: false, error_number: 1, error: errors[1] }
}
/* 2nd criterion: move number field is a integer value > 0? */
if (isNaN(tokens[5]) || parseInt(tokens[5], 10) <= 0) {
return { valid: false, error_number: 2, error: errors[2] }
}
/* 3rd criterion: half move counter is an integer >= 0? */
if (isNaN(tokens[4]) || parseInt(tokens[4], 10) < 0) {
return { valid: false, error_number: 3, error: errors[3] }
}
/* 4th criterion: 4th field is a valid e.p.-string? */
if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) {
return { valid: false, error_number: 4, error: errors[4] }
}
/* 5th criterion: 3th field is a valid castle-string? */
if (!/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(tokens[2])) {
return { valid: false, error_number: 5, error: errors[5] }
}
/* 6th criterion: 2nd field is "w" (white) or "b" (black)? */
if (!/^(w|b)$/.test(tokens[1])) {
return { valid: false, error_number: 6, error: errors[6] }
}
/* 7th criterion: 1st field contains 8 rows? */
var rows = tokens[0].split('/')
if (rows.length !== 8) {
return { valid: false, error_number: 7, error: errors[7] }
}
/* 8th criterion: every row is valid? */
for (var i = 0; i < rows.length; i++) {
/* check for right sum of fields AND not two numbers in succession */
var sum_fields = 0
var previous_was_number = false
for (var k = 0; k < rows[i].length; k++) {
if (!isNaN(rows[i][k])) {
if (previous_was_number) {
return { valid: false, error_number: 8, error: errors[8] }
}
sum_fields += parseInt(rows[i][k], 10)
previous_was_number = true
} else {
if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) {
return { valid: false, error_number: 9, error: errors[9] }
}
sum_fields += 1
previous_was_number = false
}
}
if (sum_fields !== 8) {
return { valid: false, error_number: 10, error: errors[10] }
}
}
if (
(tokens[3][1] == '3' && tokens[1] == 'w') ||
(tokens[3][1] == '6' && tokens[1] == 'b')
) {
return { valid: false, error_number: 11, error: errors[11] }
}
/* everything's okay! */
return { valid: true, error_number: 0, error: errors[0] }
}
function generate_fen() {
var empty = 0
var fen = ''
for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
if (board[i] == null) {
empty++
} else {
if (empty > 0) {
fen += empty
empty = 0
}
var color = board[i].color
var piece = board[i].type
fen += color === WHITE ? piece.toUpperCase() : piece.toLowerCase()
}
if ((i + 1) & 0x88) {
if (empty > 0) {
fen += empty
}
if (i !== SQUARES.h1) {
fen += '/'
}
empty = 0
i += 8
}
}
var cflags = ''
if (castling[WHITE] & BITS.KSIDE_CASTLE) {
cflags += 'K'
}
if (castling[WHITE] & BITS.QSIDE_CASTLE) {
cflags += 'Q'
}
if (castling[BLACK] & BITS.KSIDE_CASTLE) {
cflags += 'k'
}
if (castling[BLACK] & BITS.QSIDE_CASTLE) {
cflags += 'q'
}
/* do we have an empty castling flag? */
cflags = cflags || '-'
var epflags = ep_square === EMPTY ? '-' : algebraic(ep_square)
return [fen, turn, cflags, epflags, half_moves, move_number].join(' ')
}
function set_header(args) {
for (var i = 0; i < args.length; i += 2) {
if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') {
header[args[i]] = args[i + 1]
}
}
return header
}
/* called when the initial board setup is changed with put() or remove().
* modifies the SetUp and FEN properties of the header object. if the FEN is
* equal to the default position, the SetUp and FEN are deleted
* the setup is only updated if history.length is zero, ie moves haven't been
* made.
*/
function update_setup(fen) {
if (history.length > 0) return
if (fen !== DEFAULT_POSITION) {
header['SetUp'] = '1'
header['FEN'] = fen
} else {
delete header['SetUp']
delete header['FEN']
}
}
function get(square) {
var piece = board[SQUARES[square]]
return piece ? { type: piece.type, color: piece.color } : null
}
function put(piece, square) {
/* check for valid piece object */
if (!('type' in piece && 'color' in piece)) {
return false
}
/* check for piece */
if (SYMBOLS.indexOf(piece.type.toLowerCase()) === -1) {
return false
}
/* check for valid square */
if (!(square in SQUARES)) {
return false
}
var sq = SQUARES[square]
/* don't let the user place more than one king */
if (
piece.type == KING &&
!(kings[piece.color] == EMPTY || kings[piece.color] == sq)
) {
return false
}
board[sq] = { type: piece.type, color: piece.color }
if (piece.type === KING) {
kings[piece.color] = sq
}
update_setup(generate_fen())
return true
}
function remove(square) {
var piece = get(square)
board[SQUARES[square]] = null
if (piece && piece.type === KING) {
kings[piece.color] = EMPTY
}
update_setup(generate_fen())
return piece
}
function build_move(board, from, to, flags, promotion) {
var move = {
color: turn,
from: from,
to: to,
flags: flags,
piece: board[from].type
}
if (promotion) {
move.flags |= BITS.PROMOTION
move.promotion = promotion
}
if (board[to]) {
move.captured = board[to].type
} else if (flags & BITS.EP_CAPTURE) {
move.captured = PAWN
}
return move
}
function generate_moves(options) {
function add_move(board, moves, from, to, flags) {
/* if pawn promotion */
if (
board[from].type === PAWN &&
(rank(to) === RANK_8 || rank(to) === RANK_1)
) {
var pieces = [QUEEN, ROOK, BISHOP, KNIGHT]
for (var i = 0, len = pieces.length; i < len; i++) {
moves.push(build_move(board, from, to, flags, pieces[i]))
}
} else {
moves.push(build_move(board, from, to, flags))
}
}
var moves = []
var us = turn
var them = swap_color(us)
var second_rank = { b: RANK_7, w: RANK_2 }
var first_sq = SQUARES.a8
var last_sq = SQUARES.h1
var single_square = false
/* do we want legal moves? */
var legal =
typeof options !== 'undefined' && 'legal' in options
? options.legal
: true
/* are we generating moves for a single square? */
if (typeof options !== 'undefined' && 'square' in options) {
if (options.square in SQUARES) {
first_sq = last_sq = SQUARES[options.square]
single_square = true
} else {
/* invalid square */
return []
}
}
for (var i = first_sq; i <= last_sq; i++) {
/* did we run off the end of the board */
if (i & 0x88) {
i += 7
continue
}
var piece = board[i]
if (piece == null || piece.color !== us) {
continue
}
if (piece.type === PAWN) {
/* single square, non-capturing */
var square = i + PAWN_OFFSETS[us][0]
if (board[square] == null) {
add_move(board, moves, i, square, BITS.NORMAL)
/* double square */
var square = i + PAWN_OFFSETS[us][1]
if (second_rank[us] === rank(i) && board[square] == null) {
add_move(board, moves, i, square, BITS.BIG_PAWN)
}
}
/* pawn captures */
for (j = 2; j < 4; j++) {
var square = i + PAWN_OFFSETS[us][j]
if (square & 0x88) continue
if (board[square] != null && board[square].color === them) {
add_move(board, moves, i, square, BITS.CAPTURE)
} else if (square === ep_square) {
add_move(board, moves, i, ep_square, BITS.EP_CAPTURE)
}
}
} else {
for (var j = 0, len = PIECE_OFFSETS[piece.type].length; j < len; j++) {
var offset = PIECE_OFFSETS[piece.type][j]
var square = i
while (true) {
square += offset
if (square & 0x88) break
if (board[square] == null) {
add_move(board, moves, i, square, BITS.NORMAL)
} else {
if (board[square].color === us) break
add_move(board, moves, i, square, BITS.CAPTURE)
break
}
/* break, if knight or king */
if (piece.type === 'n' || piece.type === 'k') break
}
}
}
}
/* check for castling if: a) we're generating all moves, or b) we're doing
* single square move generation on the king's square
*/
if (!single_square || last_sq === kings[us]) {
/* king-side castling */
if (castling[us] & BITS.KSIDE_CASTLE) {
var castling_from = kings[us]
var castling_to = castling_from + 2
if (
board[castling_from + 1] == null &&
board[castling_to] == null &&
!attacked(them, kings[us]) &&
!attacked(them, castling_from + 1) &&
!attacked(them, castling_to)
) {
add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE)
}
}
/* queen-side castling */
if (castling[us] & BITS.QSIDE_CASTLE) {
var castling_from = kings[us]
var castling_to = castling_from - 2
if (
board[castling_from - 1] == null &&
board[castling_from - 2] == null &&
board[castling_from - 3] == null &&
!attacked(them, kings[us]) &&
!attacked(them, castling_from - 1) &&
!attacked(them, castling_to)
) {
add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE)
}
}
}
/* return all pseudo-legal moves (this includes moves that allow the king
* to be captured)
*/
if (!legal) {
return moves
}
/* filter out illegal moves */
var legal_moves = []
for (var i = 0, len = moves.length; i < len; i++) {
make_move(moves[i])
if (!king_attacked(us)) {
legal_moves.push(moves[i])
}
undo_move()
}
return legal_moves
}
/* convert a move from 0x88 coordinates to Standard Algebraic Notation
* (SAN)
*
* @param {boolean} sloppy Use the sloppy SAN generator to work around over
* disambiguation bugs in Fritz and Chessbase. See below:
*
* r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4
* 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned
* 4. ... Ne7 is technically the valid SAN
*/
function move_to_san(move, sloppy) {
var output = ''
if (move.flags & BITS.KSIDE_CASTLE) {
output = 'O-O'
} else if (move.flags & BITS.QSIDE_CASTLE) {
output = 'O-O-O'
} else {
var disambiguator = get_disambiguator(move, sloppy)
if (move.piece !== PAWN) {
output += move.piece.toUpperCase() + disambiguator
}
if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
if (move.piece === PAWN) {
output += algebraic(move.from)[0]
}
output += 'x'
}
output += algebraic(move.to)
if (move.flags & BITS.PROMOTION) {
output += '=' + move.promotion.toUpperCase()
}
}
make_move(move)
if (in_check()) {
if (in_checkmate()) {
output += '#'
} else {
output += '+'
}
}
undo_move()
return output
}
// parses all of the decorators out of a SAN string
function stripped_san(move) {
return move.replace(/=/, '').replace(/[+#]?[?!]*$/, '')
}
function attacked(color, square) {
for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
/* did we run off the end of the board */
if (i & 0x88) {
i += 7
continue
}
/* if empty square or wrong color */
if (board[i] == null || board[i].color !== color) continue
var piece = board[i]
var difference = i - square
var index = difference + 119
if (ATTACKS[index] & (1 << SHIFTS[piece.type])) {
if (piece.type === PAWN) {
if (difference > 0) {
if (piece.color === WHITE) return true
} else {
if (piece.color === BLACK) return true
}
continue
}
/* if the piece is a knight or a king */
if (piece.type === 'n' || piece.type === 'k') return true
var offset = RAYS[index]
var j = i + offset
var blocked = false
while (j !== square) {
if (board[j] != null) {
blocked = true
break
}
j += offset
}
if (!blocked) return true
}
}
return false
}
function king_attacked(color) {
return attacked(swap_color(color), kings[color])
}
function in_check() {
return king_attacked(turn)
}
function in_checkmate() {
return in_check() && generate_moves().length === 0
}
function in_stalemate() {
return !in_check() && generate_moves().length === 0
}
function insufficient_material() {
var pieces = {}
var bishops = []
var num_pieces = 0
var sq_color = 0
for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
sq_color = (sq_color + 1) % 2
if (i & 0x88) {
i += 7
continue
}
var piece = board[i]
if (piece) {
pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1
if (piece.type === BISHOP) {
bishops.push(sq_color)
}
num_pieces++
}
}
/* k vs. k */
if (num_pieces === 2) {
return true
} else if (
/* k vs. kn .... or .... k vs. kb */
num_pieces === 3 &&
(pieces[BISHOP] === 1 || pieces[KNIGHT] === 1)
) {
return true
} else if (num_pieces === pieces[BISHOP] + 2) {
/* kb vs. kb where any number of bishops are all on the same color */
var sum = 0
var len = bishops.length
for (var i = 0; i < len; i++) {
sum += bishops[i]
}
if (sum === 0 || sum === len) {
return true
}
}
return false
}
function in_threefold_repetition() {
/* TODO: while this function is fine for casual use, a better
* implementation would use a Zobrist key (instead of FEN). the
* Zobrist key would be maintained in the make_move/undo_move functions,
* avoiding the costly that we do below.
*/
var moves = []
var positions = {}
var repetition = false
while (true) {
var move = undo_move()
if (!move) break
moves.push(move)
}
while (true) {
/* remove the last two fields in the FEN string, they're not needed
* when checking for draw by rep */
var fen = generate_fen()
.split(' ')
.slice(0, 4)
.join(' ')
/* has the position occurred three or move times */
positions[fen] = fen in positions ? positions[fen] + 1 : 1
if (positions[fen] >= 3) {
repetition = true
}
if (!moves.length) {
break
}
make_move(moves.pop())
}
return repetition
}
function push(move) {
history.push({
move: move,
kings: { b: kings.b, w: kings.w },
turn: turn,
castling: { b: castling.b, w: castling.w },
ep_square: ep_square,
half_moves: half_moves,
move_number: move_number
})
}
function make_move(move) {
var us = turn
var them = swap_color(us)
push(move)
board[move.to] = board[move.from]
board[move.from] = null
/* if ep capture, remove the captured pawn */
if (move.flags & BITS.EP_CAPTURE) {
if (turn === BLACK) {
board[move.to - 16] = null
} else {
board[move.to + 16] = null
}
}
/* if pawn promotion, replace with new piece */
if (move.flags & BITS.PROMOTION) {
board[move.to] = { type: move.promotion, color: us }
}
/* if we moved the king */
if (board[move.to].type === KING) {
kings[board[move.to].color] = move.to
/* if we castled, move the rook next to the king */
if (move.flags & BITS.KSIDE_CASTLE) {
var castling_to = move.to - 1
var castling_from = move.to + 1
board[castling_to] = board[castling_from]
board[castling_from] = null
} else if (move.flags & BITS.QSIDE_CASTLE) {
var castling_to = move.to + 1
var castling_from = move.to - 2
board[castling_to] = board[castling_from]
board[castling_from] = null
}
/* turn off castling */
castling[us] = ''
}
/* turn off castling if we move a rook */
if (castling[us]) {
for (var i = 0, len = ROOKS[us].length; i < len; i++) {
if (
move.from === ROOKS[us][i].square &&
castling[us] & ROOKS[us][i].flag
) {
castling[us] ^= ROOKS[us][i].flag
break
}
}
}
/* turn off castling if we capture a rook */
if (castling[them]) {
for (var i = 0, len = ROOKS[them].length; i < len; i++) {
if (
move.to === ROOKS[them][i].square &&
castling[them] & ROOKS[them][i].flag
) {
castling[them] ^= ROOKS[them][i].flag
break
}
}
}
/* if big pawn move, update the en passant square */
if (move.flags & BITS.BIG_PAWN) {
if (turn === 'b') {
ep_square = move.to - 16
} else {
ep_square = move.to + 16
}
} else {
ep_square = EMPTY
}
/* reset the 50 move counter if a pawn is moved or a piece is captured */
if (move.piece === PAWN) {
half_moves = 0
} else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
half_moves = 0
} else {
half_moves++
}
if (turn === BLACK) {
move_number++
}
turn = swap_color(turn)
}
function undo_move() {
var old = history.pop()
if (old == null) {
return null
}
var move = old.move
kings = old.kings
turn = old.turn
castling = old.castling
ep_square = old.ep_square
half_moves = old.half_moves
move_number = old.move_number
var us = turn
var them = swap_color(turn)
board[move.from] = board[move.to]
board[move.from].type = move.piece // to undo any promotions
board[move.to] = null
if (move.flags & BITS.CAPTURE) {
board[move.to] = { type: move.captured, color: them }
} else if (move.flags & BITS.EP_CAPTURE) {
var index
if (us === BLACK) {
index = move.to - 16
} else {
index = move.to + 16
}
board[index] = { type: PAWN, color: them }
}
if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) {
var castling_to, castling_from
if (move.flags & BITS.KSIDE_CASTLE) {
castling_to = move.to + 1
castling_from = move.to - 1
} else if (move.flags & BITS.QSIDE_CASTLE) {
castling_to = move.to - 2
castling_from = move.to + 1
}
board[castling_to] = board[castling_from]
board[castling_from] = null
}
return move
}
/* this function is used to uniquely identify ambiguous moves */
function get_disambiguator(move, sloppy) {
var moves = generate_moves({ legal: !sloppy })
var from = move.from
var to = move.to
var piece = move.piece
var ambiguities = 0
var same_rank = 0
var same_file = 0
for (var i = 0, len = moves.length; i < len; i++) {
var ambig_from = moves[i].from
var ambig_to = moves[i].to
var ambig_piece = moves[i].piece
/* if a move of the same piece type ends on the same to square, we'll
* need to add a disambiguator to the algebraic notation
*/
if (piece === ambig_piece && from !== ambig_from && to === ambig_to) {
ambiguities++
if (rank(from) === rank(ambig_from)) {
same_rank++
}
if (file(from) === file(ambig_from)) {
same_file++
}
}
}
if (ambiguities > 0) {
/* if there exists a similar moving piece on the same rank and file as
* the move in question, use the square as the disambiguator
*/
if (same_rank > 0 && same_file > 0) {
return algebraic(from)
} else if (same_file > 0) {
/* if the moving piece rests on the same file, use the rank symbol as the
* disambiguator
*/
return algebraic(from).charAt(1)
} else {
/* else use the file symbol */
return algebraic(from).charAt(0)
}
}
return ''
}
function ascii() {
var s = ' +------------------------+\n'
for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
/* display the rank */
if (file(i) === 0) {
s += ' ' + '87654321'[rank(i)] + ' |'
}
/* empty piece */
if (board[i] == null) {
s += ' . '
} else {
var piece = board[i].type
var color = board[i].color
var symbol = color === WHITE ? piece.toUpperCase() : piece.toLowerCase()
s += ' ' + symbol + ' '
}
if ((i + 1) & 0x88) {
s += '|\n'
i += 8
}
}
s += ' +------------------------+\n'
s += ' a b c d e f g h\n'
return s
}
// convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates
function move_from_san(move, sloppy) {
// strip off any move decorations: e.g Nf3+?!
var clean_move = stripped_san(move)
// if we're using the sloppy parser run a regex to grab piece, to, and from
// this should parse invalid SAN like: Pe2-e4, Rc1c4, Qf3xf7
if (sloppy) {
var matches = clean_move.match(
/([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/
)
if (matches) {
var piece = matches[1]
var from = matches[2]
var to = matches[3]
var promotion = matches[4]
}
}
var moves = generate_moves()
for (var i = 0, len = moves.length; i < len; i++) {
// try the strict parser first, then the sloppy parser if requested
// by the user
if (
clean_move === stripped_san(move_to_san(moves[i])) ||
(sloppy && clean_move === stripped_san(move_to_san(moves[i], true)))
) {
return moves[i]
} else {
if (
matches &&
(!piece || piece.toLowerCase() == moves[i].piece) &&
SQUARES[from] == moves[i].from &&
SQUARES[to] == moves[i].to &&
(!promotion || promotion.toLowerCase() == moves[i].promotion)
) {
return moves[i]
}
}
}
return null
}
/*****************************************************************************
* UTILITY FUNCTIONS
****************************************************************************/
function rank(i) {
return i >> 4
}
function file(i) {
return i & 15
}
function algebraic(i) {
var f = file(i),
r = rank(i)
return 'abcdefgh'.substring(f, f + 1) + '87654321'.substring(r, r + 1)
}
function swap_color(c) {
return c === WHITE ? BLACK : WHITE
}
function is_digit(c) {
return '0123456789'.indexOf(c) !== -1
}
/* pretty = external move object */
function make_pretty(ugly_move) {
var move = clone(ugly_move)
move.san = move_to_san(move, false)
move.to = algebraic(move.to)
move.from = algebraic(move.from)
var flags = ''
for (var flag in BITS) {
if (BITS[flag] & move.flags) {
flags += FLAGS[flag]
}
}
move.flags = flags
return move
}
function clone(obj) {
var dupe = obj instanceof Array ? [] : {}
for (var property in obj) {
if (typeof property === 'object') {
dupe[property] = clone(obj[property])
} else {
dupe[property] = obj[property]
}
}
return dupe
}
function trim(str) {
return str.replace(/^\s+|\s+$/g, '')
}
/*****************************************************************************
* DEBUGGING UTILITIES
****************************************************************************/
function perft(depth) {
var moves = generate_moves({ legal: false })
var nodes = 0
var color = turn
for (var i = 0, len = moves.length; i < len; i++) {
make_move(moves[i])
if (!king_attacked(color)) {
if (depth - 1 > 0) {
var child_nodes = perft(depth - 1)
nodes += child_nodes
} else {
nodes++
}
}
undo_move()
}
return nodes
}
return {
/***************************************************************************
* PUBLIC CONSTANTS (is there a better way to do this?)
**************************************************************************/
WHITE: WHITE,
BLACK: BLACK,
PAWN: PAWN,
KNIGHT: KNIGHT,
BISHOP: BISHOP,
ROOK: ROOK,
QUEEN: QUEEN,
KING: KING,
SQUARES: (function() {
/* from the ECMA-262 spec (section 12.6.4):
* "The mechanics of enumerating the properties ... is
* implementation dependent"
* so: for (var sq in SQUARES) { keys.push(sq); } might not be
* ordered correctly
*/
var keys = []
for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
if (i & 0x88) {
i += 7
continue
}
keys.push(algebraic(i))
}
return keys
})(),
FLAGS: FLAGS,
/***************************************************************************
* PUBLIC API
**************************************************************************/
load: function(fen) {
return load(fen)
},
reset: function() {
return reset()
},
moves: function(options) {
/* The internal representation of a chess move is in 0x88 format, and
* not meant to be human-readable. The code below converts the 0x88
* square coordinates to algebraic coordinates. It also prunes an
* unnecessary move keys resulting from a verbose call.
*/
var ugly_moves = generate_moves(options)
var moves = []
for (var i = 0, len = ugly_moves.length; i < len; i++) {
/* does the user want a full move object (most likely not), or just
* SAN
*/
if (
typeof options !== 'undefined' &&
'verbose' in options &&
options.verbose
) {
moves.push(make_pretty(ugly_moves[i]))
} else {
moves.push(move_to_san(ugly_moves[i], false))
}
}
return moves
},
in_check: function() {
return in_check()
},
in_checkmate: function() {
return in_checkmate()
},
in_stalemate: function() {
return in_stalemate()
},
in_draw: function() {
return (
half_moves >= 100 ||
in_stalemate() ||
insufficient_material() ||
in_threefold_repetition()
)
},
insufficient_material: function() {
return insufficient_material()
},
in_threefold_repetition: function() {
return in_threefold_repetition()
},
game_over: function() {
return (
half_moves >= 100 ||
in_checkmate() ||
in_stalemate() ||
insufficient_material() ||
in_threefold_repetition()
)
},
validate_fen: function(fen) {
return validate_fen(fen)
},
fen: function() {
return generate_fen()
},
board: function() {
var output = [],
row = []
for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
if (board[i] == null) {
row.push(null)
} else {
row.push({ type: board[i].type, color: board[i].color })
}
if ((i + 1) & 0x88) {
output.push(row)
row = []
i += 8
}
}
return output
},
pgn: function(options) {
/* using the specification from http://www.chessclub.com/help/PGN-spec
* example for html usage: .pgn({ max_width: 72, newline_char: "<br />" })
*/
var newline =
typeof options === 'object' && typeof options.newline_char === 'string'
? options.newline_char
: '\n'
var max_width =
typeof options === 'object' && typeof options.max_width === 'number'
? options.max_width
: 0
var result = []
var header_exists = false
/* add the PGN header headerrmation */
for (var i in header) {
/* TODO: order of enumerated properties in header object is not
* guaranteed, see ECMA-262 spec (section 12.6.4)
*/
result.push('[' + i + ' "' + header[i] + '"]' + newline)
header_exists = true
}
if (header_exists && history.length) {
result.push(newline)
}
var append_comment = function(move_string) {
var comment = comments[generate_fen()]
if (typeof comment !== 'undefined') {
var delimiter = move_string.length > 0 ? ' ' : '';
move_string = `${move_string}${delimiter}{${comment}}`
}
return move_string
}
/* pop all of history onto reversed_history */
var reversed_history = []
while (history.length > 0) {
reversed_history.push(undo_move())
}
var moves = []
var move_string = ''
/* special case of a commented starting position with no moves */
if (reversed_history.length === 0) {
moves.push(append_comment(''))
}
/* build the list of moves. a move_string looks like: "3. e3 e6" */
while (reversed_history.length > 0) {
move_string = append_comment(move_string)
var move = reversed_history.pop()
/* if the position started with black to move, start PGN with 1. ... */
if (!history.length && move.color === 'b') {
move_string = move_number + '. ...'
} else if (move.color === 'w') {
/* store the previous generated move_string if we have one */
if (move_string.length) {
moves.push(move_string)
}
move_string = move_number + '.'
}
move_string = move_string + ' ' + move_to_san(move, false)
make_move(move)
}
/* are there any other leftover moves? */
if (move_string.length) {
moves.push(append_comment(move_string))
}
/* is there a result? */
if (typeof header.Result !== 'undefined') {
moves.push(header.Result)
}
/* history should be back to what it was before we started generating PGN,
* so join together moves
*/
if (max_width === 0) {
return result.join('') + moves.join(' ')
}
var strip = function() {
if (result.length > 0 && result[result.length - 1] === ' ') {
result.pop();
return true;
}
return false;
};
/* NB: this does not preserve comment whitespace. */
var wrap_comment = function(width, move) {
for (var token of move.split(' ')) {
if (!token) {
continue;
}
if (width + token.length > max_width) {
while (strip()) {
width--;
}
result.push(newline);
width = 0;
}
result.push(token);
width += token.length;
result.push(' ');
width++;
}
if (strip()) {
width--;
}
return width;
};
/* wrap the PGN output at max_width */
var current_width = 0
for (var i = 0; i < moves.length; i++) {
if (current_width + moves[i].length > max_width) {
if (moves[i].includes('{')) {
current_width = wrap_comment(current_width, moves[i]);
continue;
}
}
/* if the current move will push past max_width */
if (current_width + moves[i].length > max_width && i !== 0) {
/* don't end the line with whitespace */
if (result[result.length - 1] === ' ') {
result.pop()
}
result.push(newline)
current_width = 0
} else if (i !== 0) {
result.push(' ')
current_width++
}
result.push(moves[i])
current_width += moves[i].length
}
return result.join('')
},
load_pgn: function(pgn, options) {
// allow the user to specify the sloppy move parser to work around over
// disambiguation bugs in Fritz and Chessbase
var sloppy =
typeof options !== 'undefined' && 'sloppy' in options
? options.sloppy
: false
function mask(str) {
return str.replace(/\\/g, '\\')
}
function has_keys(object) {
for (var key in object) {
return true
}
return false
}
function parse_pgn_header(header, options) {
var newline_char =
typeof options === 'object' &&
typeof options.newline_char === 'string'
? options.newline_char
: '\r?\n'
var header_obj = {}
var headers = header.split(new RegExp(mask(newline_char)))
var key = ''
var value = ''
for (var i = 0; i < headers.length; i++) {
key = headers[i].replace(/^\[([A-Z][A-Za-z]*)\s.*\]$/, '$1')
value = headers[i].replace(/^\[[A-Za-z]+\s"(.*)"\ *\]$/, '$1')
if (trim(key).length > 0) {
header_obj[key] = value
}
}
return header_obj
}
var newline_char =
typeof options === 'object' && typeof options.newline_char === 'string'
? options.newline_char
: '\r?\n'
// RegExp to split header. Takes advantage of the fact that header and movetext
// will always have a blank line between them (ie, two newline_char's).
// With default newline_char, will equal: /^(\[((?:\r?\n)|.)*\])(?:\r?\n){2}/
var header_regex = new RegExp(
'^(\\[((?:' +
mask(newline_char) +
')|.)*\\])' +
'(?:' +
mask(newline_char) +
'){2}'
)
// If no header given, begin with moves.
var header_string = header_regex.test(pgn)
? header_regex.exec(pgn)[1]
: ''
// Put the board in the starting position
reset()
/* parse PGN header */
var headers = parse_pgn_header(header_string, options)
for (var key in headers) {
set_header([key, headers[key]])
}
/* load the starting position indicated by [Setup '1'] and
* [FEN position] */
if (headers['SetUp'] === '1') {
if (!('FEN' in headers && load(headers['FEN'], true))) {
// second argument to load: don't clear the headers
return false
}
}
/* NB: the regexes below that delete move numbers, recursive
* annotations, and numeric annotation glyphs may also match
* text in comments. To prevent this, we transform comments
* by hex-encoding them in place and decoding them again after
* the other tokens have been deleted.
*
* While the spec states that PGN files should be ASCII encoded,
* we use {en,de}codeURIComponent here to support arbitrary UTF8
* as a convenience for modern users */
var to_hex = function(string) {
return Array
.from(string)
.map(function(c) {
/* encodeURI doesn't transform most ASCII characters,
* so we handle these ourselves */
return c.charCodeAt(0) < 128
? c.charCodeAt(0).toString(16)
: encodeURIComponent(c).replace(/\%/g, '').toLowerCase()
})
.join('')
}
var from_hex = function(string) {
return string.length == 0
? ''
: decodeURIComponent('%' + string.match(/.{1,2}/g).join('%'))
}
var encode_comment = function(string) {
string = string.replace(new RegExp(mask(newline_char), 'g'), ' ')
return `{${to_hex(string.slice(1, string.length - 1))}}`
}
var decode_comment = function(string) {
if (string.startsWith('{') && string.endsWith('}')) {
return from_hex(string.slice(1, string.length - 1))
}
}
/* delete header to get the moves */
var ms = pgn
.replace(header_string, '')
.replace(
/* encode comments so they don't get deleted below */
new RegExp(`(\{[^}]*\})+?|;([^${mask(newline_char)}]*)`, 'g'),
function(match, bracket, semicolon) {
return bracket !== undefined
? encode_comment(bracket)
: ' ' + encode_comment(`{${semicolon.slice(1)}}`)
}
)
.replace(new RegExp(mask(newline_char), 'g'), ' ')
/* delete recursive annotation variations */
var rav_regex = /(\([^\(\)]+\))+?/g
while (rav_regex.test(ms)) {
ms = ms.replace(rav_regex, '')
}
/* delete move numbers */
ms = ms.replace(/\d+\.(\.\.)?/g, '')
/* delete ... indicating black to move */
ms = ms.replace(/\.\.\./g, '')
/* delete numeric annotation glyphs */
ms = ms.replace(/\$\d+/g, '')
/* trim and get array of moves */
var moves = trim(ms).split(new RegExp(/\s+/))
/* delete empty entries */
moves = moves
.join(',')
.replace(/,,+/g, ',')
.split(',')
var move = ''
for (var half_move = 0; half_move < moves.length - 1; half_move++) {
var comment = decode_comment(moves[half_move])
if (comment !== undefined) {
comments[generate_fen()] = comment
continue
}
move = move_from_san(moves[half_move], sloppy)
/* move not possible! (don't clear the board to examine to show the
* latest valid position)
*/
if (move == null) {
return false
} else {
make_move(move)
}
}
comment = decode_comment(moves[moves.length - 1])
if (comment !== undefined) {
comments[generate_fen()] = comment
moves.pop()
}
/* examine last move */
move = moves[moves.length - 1]
if (POSSIBLE_RESULTS.indexOf(move) > -1) {
if (has_keys(header) && typeof header.Result === 'undefined') {
set_header(['Result', move])
}
} else {
move = move_from_san(move, sloppy)
if (move == null) {
return false
} else {
make_move(move)
}
}
return true
},
header: function() {
return set_header(arguments)
},
ascii: function() {
return ascii()
},
turn: function() {
return turn
},
move: function(move, options) {
/* The move function can be called with in the following parameters:
*
* .move('Nxb7') <- where 'move' is a case-sensitive SAN string
*
* .move({ from: 'h7', <- where the 'move' is a move object (additional
* to :'h8', fields are ignored)
* promotion: 'q',
* })
*/
// allow the user to specify the sloppy move parser to work around over
// disambiguation bugs in Fritz and Chessbase
var sloppy =
typeof options !== 'undefined' && 'sloppy' in options
? options.sloppy
: false
var move_obj = null
if (typeof move === 'string') {
move_obj = move_from_san(move, sloppy)
} else if (typeof move === 'object') {
var moves = generate_moves()
/* convert the pretty move object to an ugly move object */
for (var i = 0, len = moves.length; i < len; i++) {
if (
move.from === algebraic(moves[i].from) &&
move.to === algebraic(moves[i].to) &&
(!('promotion' in moves[i]) ||
move.promotion === moves[i].promotion)
) {
move_obj = moves[i]
break
}
}
}
/* failed to find move */
if (!move_obj) {
return null
}
/* need to make a copy of move because we can't generate SAN after the
* move is made
*/
var pretty_move = make_pretty(move_obj)
make_move(move_obj)
return pretty_move
},
undo: function() {
var move = undo_move()
return move ? make_pretty(move) : null
},
clear: function() {
return clear()
},
put: function(piece, square) {
return put(piece, square)
},
get: function(square) {
return get(square)
},
remove: function(square) {
return remove(square)
},
perft: function(depth) {
return perft(depth)
},
square_color: function(square) {
if (square in SQUARES) {
var sq_0x88 = SQUARES[square]
return (rank(sq_0x88) + file(sq_0x88)) % 2 === 0 ? 'light' : 'dark'
}
return null
},
history: function(options) {
var reversed_history = []
var move_history = []
var verbose =
typeof options !== 'undefined' &&
'verbose' in options &&
options.verbose
while (history.length > 0) {
reversed_history.push(undo_move())
}
while (reversed_history.length > 0) {
var move = reversed_history.pop()
if (verbose) {
move_history.push(make_pretty(move))
} else {
move_history.push(move_to_san(move))
}
make_move(move)
}
return move_history
},
get_comment: function() {
return comments[generate_fen()];
},
set_comment: function(comment) {
comments[generate_fen()] = comment.replace('{', '[').replace('}', ']');
},
delete_comment: function() {
var comment = comments[generate_fen()];
delete comments[generate_fen()];
return comment;
},
get_comments: function() {
prune_comments();
return Object.keys(comments).map(function(fen) {
return {fen: fen, comment: comments[fen]};
});
},
delete_comments: function() {
prune_comments();
return Object.keys(comments)
.map(function(fen) {
var comment = comments[fen];
delete comments[fen];
return {fen: fen, comment: comment};
});
}
}
}
/* export Chess object if using node or any other CommonJS compatible
* environment */
if (typeof exports !== 'undefined') exports.Chess = Chess
/* export Chess object for any RequireJS compatible environment */
if (typeof define !== 'undefined')
define(function() {
return Chess
})
.cg-wrap {
box-sizing: content-box;
position: relative;
display: block;
}
cg-container {
position: absolute;
width: 100%;
height: 100%;
display: block;
top: 0;
}
cg-board {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
line-height: 0;
background-size: cover;
cursor: pointer;
}
cg-board square {
position: absolute;
top: 0;
left: 0;
width: 12.5%;
height: 12.5%;
pointer-events: none;
}
cg-board square.move-dest {
pointer-events: auto;
}
cg-board square.last-move {
will-change: transform;
}
.cg-wrap piece {
position: absolute;
top: 0;
left: 0;
width: 12.5%;
height: 12.5%;
background-size: cover;
z-index: 2;
will-change: transform;
pointer-events: none;
}
cg-board piece.dragging {
cursor: move;
/* !important to override z-index from 3D piece inline style */
z-index: 11 !important;
}
piece.anim {
z-index: 8;
}
piece.fading {
z-index: 1;
opacity: 0.5;
}
.cg-wrap piece.ghost {
opacity: 0.3;
}
.cg-wrap piece svg {
overflow: hidden;
position: relative;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 2;
opacity: 0.6;
}
.cg-wrap piece svg image {
opacity: 0.5;
}
.cg-wrap .cg-shapes,
.cg-wrap .cg-custom-svgs {
overflow: hidden;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
pointer-events: none;
}
.cg-wrap .cg-shapes {
opacity: 0.6;
z-index: 2;
}
.cg-wrap .cg-custom-svgs {
/* over piece.anim = 8, but under piece.dragging = 11 */
z-index: 9;
overflow: visible;
}
.cg-wrap .cg-custom-svgs svg {
overflow: visible;
}
.cg-wrap coords {
position: absolute;
display: flex;
pointer-events: none;
opacity: 0.8;
font-family: sans-serif;
font-size: 9px;
}
.cg-wrap coords.ranks {
left: 4px;
top: -20px;
flex-flow: column-reverse;
height: 100%;
width: 12px;
}
.cg-wrap coords.ranks.black {
flex-flow: column;
}
.cg-wrap coords.files {
bottom: -4px;
left: 24px;
flex-flow: row;
width: 100%;
height: 16px;
text-transform: uppercase;
text-align: center;
}
.cg-wrap coords.files.black {
flex-flow: row-reverse;
}
.cg-wrap coords coord {
flex: 1 1 auto;
}
.cg-wrap coords.ranks coord {
transform: translateY(39%);
}
/** Colored board squares as an embedded SVG */
cg-board {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4PSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIgogICAgIHZpZXdCb3g9IjAgMCA4IDgiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CjxnIGlkPSJhIj4KICA8ZyBpZD0iYiI+CiAgICA8ZyBpZD0iYyI+CiAgICAgIDxnIGlkPSJkIj4KICAgICAgICA8cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSIjZGVlM2U2IiBpZD0iZSIvPgogICAgICAgIDx1c2UgeD0iMSIgeT0iMSIgaHJlZj0iI2UiIHg6aHJlZj0iI2UiLz4KICAgICAgICA8cmVjdCB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSIjOGNhMmFkIiBpZD0iZiIvPgogICAgICAgIDx1c2UgeD0iMSIgeT0iLTEiIGhyZWY9IiNmIiB4OmhyZWY9IiNmIi8+CiAgICAgIDwvZz4KICAgICAgPHVzZSB4PSIyIiBocmVmPSIjZCIgeDpocmVmPSIjZCIvPgogICAgPC9nPgogICAgPHVzZSB4PSI0IiBocmVmPSIjYyIgeDpocmVmPSIjYyIvPgogIDwvZz4KICA8dXNlIHk9IjIiIGhyZWY9IiNiIiB4OmhyZWY9IiNiIi8+CjwvZz4KPHVzZSB5PSI0IiBocmVmPSIjYSIgeDpocmVmPSIjYSIvPgo8L3N2Zz4K');
}
/** Interactive board square colors */
cg-board square.move-dest {
background: radial-gradient(rgba(20, 85, 30, 0.5) 22%, #208530 0, rgba(0, 0, 0, 0.3) 0, rgba(0, 0, 0, 0) 0);
}
cg-board square.premove-dest {
background: radial-gradient(rgba(20, 30, 85, 0.5) 22%, #203085 0, rgba(0, 0, 0, 0.3) 0, rgba(0, 0, 0, 0) 0);
}
cg-board square.oc.move-dest {
background: radial-gradient(transparent 0%, transparent 80%, rgba(20, 85, 0, 0.3) 80%);
}
cg-board square.oc.premove-dest {
background: radial-gradient(transparent 0%, transparent 80%, rgba(20, 30, 85, 0.2) 80%);
}
cg-board square.move-dest:hover {
background: rgba(20, 85, 30, 0.3);
}
cg-board square.premove-dest:hover {
background: rgba(20, 30, 85, 0.2);
}
cg-board square.last-move {
background-color: rgba(155, 199, 0, 0.41);
}
cg-board square.selected {
background-color: rgba(20, 85, 30, 0.5);
}
cg-board square.check {
background: radial-gradient(
ellipse at center,
rgba(255, 0, 0, 1) 0%,
rgba(231, 0, 0, 1) 25%,
rgba(169, 0, 0, 0) 89%,
rgba(158, 0, 0, 0) 100%
);
}
cg-board square.current-premove {
background-color: rgba(20, 30, 85, 0.5);
}
/** Alternating colors in rank/file labels */
.cg-wrap.orientation-white coords.ranks coord:nth-child(2n),
.cg-wrap.orientation-white coords.files coord:nth-child(2n),
.cg-wrap.orientation-black coords.ranks coord:nth-child(2n + 1),
.cg-wrap.orientation-black coords.files coord:nth-child(2n + 1) {
color: #b58863;
}
.cg-wrap.orientation-black coords.ranks coord:nth-child(2n),
.cg-wrap.orientation-black coords.files coord:nth-child(2n),
.cg-wrap.orientation-white coords.ranks coord:nth-child(2n + 1),
.cg-wrap.orientation-white coords.files coord:nth-child(2n + 1) {
color: #f0d9b5;
}
/** Embedded SVGs for all chess pieces */
.cg-wrap piece.pawn.white {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PHBhdGggZD0iTTIyLjUgOWMtMi4yMSAwLTQgMS43OS00IDQgMCAuODkuMjkgMS43MS43OCAyLjM4QzE3LjMzIDE2LjUgMTYgMTguNTkgMTYgMjFjMCAyLjAzLjk0IDMuODQgMi40MSA1LjAzLTMgMS4wNi03LjQxIDUuNTUtNy40MSAxMy40N2gyM2MwLTcuOTItNC40MS0xMi40MS03LjQxLTEzLjQ3IDEuNDctMS4xOSAyLjQxLTMgMi40MS01LjAzIDAtMi40MS0xLjMzLTQuNS0zLjI4LTUuNjIuNDktLjY3Ljc4LTEuNDkuNzgtMi4zOCAwLTIuMjEtMS43OS00LTQtNHoiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==');
}
.cg-wrap piece.bishop.white {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxnIGZpbGw9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJidXR0Ij48cGF0aCBkPSJNOSAzNmMzLjM5LS45NyAxMC4xMS40MyAxMy41LTIgMy4zOSAyLjQzIDEwLjExIDEuMDMgMTMuNSAyIDAgMCAxLjY1LjU0IDMgMi0uNjguOTctMS42NS45OS0zIC41LTMuMzktLjk3LTEwLjExLjQ2LTEzLjUtMS0zLjM5IDEuNDYtMTAuMTEuMDMtMTMuNSAxLTEuMzU0LjQ5LTIuMzIzLjQ3LTMtLjUgMS4zNTQtMS45NCAzLTIgMy0yeiIvPjxwYXRoIGQ9Ik0xNSAzMmMyLjUgMi41IDEyLjUgMi41IDE1IDAgLjUtMS41IDAtMiAwLTIgMC0yLjUtMi41LTQtMi41LTQgNS41LTEuNSA2LTExLjUtNS0xNS41LTExIDQtMTAuNSAxNC01IDE1LjUgMCAwLTIuNSAxLjUtMi41IDQgMCAwLS41LjUgMCAyeiIvPjxwYXRoIGQ9Ik0yNSA4YTIuNSAyLjUgMCAxIDEtNSAwIDIuNSAyLjUgMCAxIDEgNSAweiIvPjwvZz48cGF0aCBkPSJNMTcuNSAyNmgxME0xNSAzMGgxNW0tNy41LTE0LjV2NU0yMCAxOGg1IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+PC9nPjwvc3ZnPg==');
}
.cg-wrap piece.knight.white {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMiAxMGMxMC41IDEgMTYuNSA4IDE2IDI5SDE1YzAtOSAxMC02LjUgOC0yMSIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0yNCAxOGMuMzggMi45MS01LjU1IDcuMzctOCA5LTMgMi0yLjgyIDQuMzQtNSA0LTEuMDQyLS45NCAxLjQxLTMuMDQgMC0zLTEgMCAuMTkgMS4yMy0xIDItMSAwLTQuMDAzIDEtNC00IDAtMiA2LTEyIDYtMTJzMS44OS0xLjkgMi0zLjVjLS43My0uOTk0LS41LTItLjUtMyAxLTEgMyAyLjUgMyAyLjVoMnMuNzgtMS45OTIgMi41LTNjMSAwIDEgMyAxIDMiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNOS41IDI1LjVhLjUuNSAwIDEgMS0xIDAgLjUuNSAwIDEgMSAxIDB6bTUuNDMzLTkuNzVhLjUgMS41IDMwIDEgMS0uODY2LS41LjUgMS41IDMwIDEgMSAuODY2LjV6IiBmaWxsPSIjMDAwIi8+PC9nPjwvc3ZnPg==');
}
.cg-wrap piece.rook.white {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05IDM5aDI3di0zSDl2M3ptMy0zdi00aDIxdjRIMTJ6bS0xLTIyVjloNHYyaDVWOWg1djJoNVY5aDR2NSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNMzQgMTRsLTMgM0gxNGwtMy0zIi8+PHBhdGggZD0iTTMxIDE3djEyLjVIMTRWMTciIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+PHBhdGggZD0iTTMxIDI5LjVsMS41IDIuNWgtMjBsMS41LTIuNSIvPjxwYXRoIGQ9Ik0xMSAxNGgyMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPjwvZz48L3N2Zz4=');
}
.cg-wrap piece.queen.white {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik04IDEyYTIgMiAwIDEgMS00IDAgMiAyIDAgMSAxIDQgMHptMTYuNS00LjVhMiAyIDAgMSAxLTQgMCAyIDIgMCAxIDEgNCAwek00MSAxMmEyIDIgMCAxIDEtNCAwIDIgMiAwIDEgMSA0IDB6TTE2IDguNWEyIDIgMCAxIDEtNCAwIDIgMiAwIDEgMSA0IDB6TTMzIDlhMiAyIDAgMSAxLTQgMCAyIDIgMCAxIDEgNCAweiIvPjxwYXRoIGQ9Ik05IDI2YzguNS0xLjUgMjEtMS41IDI3IDBsMi0xMi03IDExVjExbC01LjUgMTMuNS0zLTE1LTMgMTUtNS41LTE0VjI1TDcgMTRsMiAxMnoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTkgMjZjMCAyIDEuNSAyIDIuNSA0IDEgMS41IDEgMSAuNSAzLjUtMS41IDEtMS41IDIuNS0xLjUgMi41LTEuNSAxLjUuNSAyLjUuNSAyLjUgNi41IDEgMTYuNSAxIDIzIDAgMCAwIDEuNS0xIDAtMi41IDAgMCAuNS0xLjUtMS0yLjUtLjUtMi41LS41LTIgLjUtMy41IDEtMiAyLjUtMiAyLjUtNC04LjUtMS41LTE4LjUtMS41LTI3IDB6IiBzdHJva2UtbGluZWNhcD0iYnV0dCIvPjxwYXRoIGQ9Ik0xMS41IDMwYzMuNS0xIDE4LjUtMSAyMiAwTTEyIDMzLjVjNi0xIDE1LTEgMjEgMCIgZmlsbD0ibm9uZSIvPjwvZz48L3N2Zz4=');
}
.cg-wrap piece.king.white {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMi41IDExLjYzVjZNMjAgOGg1IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+PHBhdGggZD0iTTIyLjUgMjVzNC41LTcuNSAzLTEwLjVjMCAwLTEtMi41LTMtMi41cy0zIDIuNS0zIDIuNWMtMS41IDMgMyAxMC41IDMgMTAuNSIgZmlsbD0iI2ZmZiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMTEuNSAzN2M1LjUgMy41IDE1LjUgMy41IDIxIDB2LTdzOS00LjUgNi0xMC41Yy00LTYuNS0xMy41LTMuNS0xNiA0VjI3di0zLjVjLTMuNS03LjUtMTMtMTAuNS0xNi00LTMgNiA1IDEwIDUgMTBWMzd6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTExLjUgMzBjNS41LTMgMTUuNS0zIDIxIDBtLTIxIDMuNWM1LjUtMyAxNS41LTMgMjEgMG0tMjEgMy41YzUuNS0zIDE1LjUtMyAyMSAwIi8+PC9nPjwvc3ZnPg==');
}
.cg-wrap piece.pawn.black {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PHBhdGggZD0iTTIyLjUgOWMtMi4yMSAwLTQgMS43OS00IDQgMCAuODkuMjkgMS43MS43OCAyLjM4QzE3LjMzIDE2LjUgMTYgMTguNTkgMTYgMjFjMCAyLjAzLjk0IDMuODQgMi40MSA1LjAzLTMgMS4wNi03LjQxIDUuNTUtNy40MSAxMy40N2gyM2MwLTcuOTItNC40MS0xMi40MS03LjQxLTEzLjQ3IDEuNDctMS4xOSAyLjQxLTMgMi40MS01LjAzIDAtMi40MS0xLjMzLTQuNS0zLjI4LTUuNjIuNDktLjY3Ljc4LTEuNDkuNzgtMi4zOCAwLTIuMjEtMS43OS00LTQtNHoiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==');
}
.cg-wrap piece.bishop.black {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxnIGZpbGw9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJidXR0Ij48cGF0aCBkPSJNOSAzNmMzLjM5LS45NyAxMC4xMS40MyAxMy41LTIgMy4zOSAyLjQzIDEwLjExIDEuMDMgMTMuNSAyIDAgMCAxLjY1LjU0IDMgMi0uNjguOTctMS42NS45OS0zIC41LTMuMzktLjk3LTEwLjExLjQ2LTEzLjUtMS0zLjM5IDEuNDYtMTAuMTEuMDMtMTMuNSAxLTEuMzU0LjQ5LTIuMzIzLjQ3LTMtLjUgMS4zNTQtMS45NCAzLTIgMy0yeiIvPjxwYXRoIGQ9Ik0xNSAzMmMyLjUgMi41IDEyLjUgMi41IDE1IDAgLjUtMS41IDAtMiAwLTIgMC0yLjUtMi41LTQtMi41LTQgNS41LTEuNSA2LTExLjUtNS0xNS41LTExIDQtMTAuNSAxNC01IDE1LjUgMCAwLTIuNSAxLjUtMi41IDQgMCAwLS41LjUgMCAyeiIvPjxwYXRoIGQ9Ik0yNSA4YTIuNSAyLjUgMCAxIDEtNSAwIDIuNSAyLjUgMCAxIDEgNSAweiIvPjwvZz48cGF0aCBkPSJNMTcuNSAyNmgxME0xNSAzMGgxNW0tNy41LTE0LjV2NU0yMCAxOGg1IiBzdHJva2U9IiNlY2VjZWMiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48L2c+PC9zdmc+');
}
.cg-wrap piece.knight.black {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMiAxMGMxMC41IDEgMTYuNSA4IDE2IDI5SDE1YzAtOSAxMC02LjUgOC0yMSIgZmlsbD0iIzAwMCIvPjxwYXRoIGQ9Ik0yNCAxOGMuMzggMi45MS01LjU1IDcuMzctOCA5LTMgMi0yLjgyIDQuMzQtNSA0LTEuMDQyLS45NCAxLjQxLTMuMDQgMC0zLTEgMCAuMTkgMS4yMy0xIDItMSAwLTQuMDAzIDEtNC00IDAtMiA2LTEyIDYtMTJzMS44OS0xLjkgMi0zLjVjLS43My0uOTk0LS41LTItLjUtMyAxLTEgMyAyLjUgMyAyLjVoMnMuNzgtMS45OTIgMi41LTNjMSAwIDEgMyAxIDMiIGZpbGw9IiMwMDAiLz48cGF0aCBkPSJNOS41IDI1LjVhLjUuNSAwIDEgMS0xIDAgLjUuNSAwIDEgMSAxIDB6bTUuNDMzLTkuNzVhLjUgMS41IDMwIDEgMS0uODY2LS41LjUgMS41IDMwIDEgMSAuODY2LjV6IiBmaWxsPSIjZWNlY2VjIiBzdHJva2U9IiNlY2VjZWMiLz48cGF0aCBkPSJNMjQuNTUgMTAuNGwtLjQ1IDEuNDUuNS4xNWMzLjE1IDEgNS42NSAyLjQ5IDcuOSA2Ljc1UzM1Ljc1IDI5LjA2IDM1LjI1IDM5bC0uMDUuNWgyLjI1bC4wNS0uNWMuNS0xMC4wNi0uODgtMTYuODUtMy4yNS0yMS4zNC0yLjM3LTQuNDktNS43OS02LjY0LTkuMTktNy4xNmwtLjUxLS4xeiIgZmlsbD0iI2VjZWNlYyIgc3Ryb2tlPSJub25lIi8+PC9nPjwvc3ZnPg==');
}
.cg-wrap piece.rook.black {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05IDM5aDI3di0zSDl2M3ptMy41LTdsMS41LTIuNWgxN2wxLjUgMi41aC0yMHptLS41IDR2LTRoMjF2NEgxMnoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTE0IDI5LjV2LTEzaDE3djEzSDE0eiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMTQgMTYuNUwxMSAxNGgyM2wtMyAyLjVIMTR6TTExIDE0VjloNHYyaDVWOWg1djJoNVY5aDR2NUgxMXoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTEyIDM1LjVoMjFtLTIwLTRoMTltLTE4LTJoMTdtLTE3LTEzaDE3TTExIDE0aDIzIiBmaWxsPSJub25lIiBzdHJva2U9IiNlY2VjZWMiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPjwvZz48L3N2Zz4=');
}
.cg-wrap piece.queen.black {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxnIHN0cm9rZT0ibm9uZSI+PGNpcmNsZSBjeD0iNiIgY3k9IjEyIiByPSIyLjc1Ii8+PGNpcmNsZSBjeD0iMTQiIGN5PSI5IiByPSIyLjc1Ii8+PGNpcmNsZSBjeD0iMjIuNSIgY3k9IjgiIHI9IjIuNzUiLz48Y2lyY2xlIGN4PSIzMSIgY3k9IjkiIHI9IjIuNzUiLz48Y2lyY2xlIGN4PSIzOSIgY3k9IjEyIiByPSIyLjc1Ii8+PC9nPjxwYXRoIGQ9Ik05IDI2YzguNS0xLjUgMjEtMS41IDI3IDBsMi41LTEyLjVMMzEgMjVsLS4zLTE0LjEtNS4yIDEzLjYtMy0xNC41LTMgMTQuNS01LjItMTMuNkwxNCAyNSA2LjUgMTMuNSA5IDI2eiIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNOSAyNmMwIDIgMS41IDIgMi41IDQgMSAxLjUgMSAxIC41IDMuNS0xLjUgMS0xLjUgMi41LTEuNSAyLjUtMS41IDEuNS41IDIuNS41IDIuNSA2LjUgMSAxNi41IDEgMjMgMCAwIDAgMS41LTEgMC0yLjUgMCAwIC41LTEuNS0xLTIuNS0uNS0yLjUtLjUtMiAuNS0zLjUgMS0yIDIuNS0yIDIuNS00LTguNS0xLjUtMTguNS0xLjUtMjcgMHoiIHN0cm9rZS1saW5lY2FwPSJidXR0Ii8+PHBhdGggZD0iTTExIDM4LjVhMzUgMzUgMSAwIDAgMjMgMCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNMTEgMjlhMzUgMzUgMSAwIDEgMjMgMG0tMjEuNSAyLjVoMjBtLTIxIDNhMzUgMzUgMSAwIDAgMjIgMG0tMjMgM2EzNSAzNSAxIDAgMCAyNCAwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlY2VjZWMiLz48L2c+PC9zdmc+');
}
.cg-wrap piece.king.black {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMi41IDExLjYzVjYiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiLz48cGF0aCBkPSJNMjIuNSAyNXM0LjUtNy41IDMtMTAuNWMwIDAtMS0yLjUtMy0yLjVzLTMgMi41LTMgMi41Yy0xLjUgMyAzIDEwLjUgMyAxMC41IiBmaWxsPSIjMDAwIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIvPjxwYXRoIGQ9Ik0xMS41IDM3YzUuNSAzLjUgMTUuNSAzLjUgMjEgMHYtN3M5LTQuNSA2LTEwLjVjLTQtNi41LTEzLjUtMy41LTE2IDRWMjd2LTMuNWMtMy41LTcuNS0xMy0xMC41LTE2LTQtMyA2IDUgMTAgNSAxMFYzN3oiIGZpbGw9IiMwMDAiLz48cGF0aCBkPSJNMjAgOGg1IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+PHBhdGggZD0iTTMyIDI5LjVzOC41LTQgNi4wMy05LjY1QzM0LjE1IDE0IDI1IDE4IDIyLjUgMjQuNWwuMDEgMi4xLS4wMS0yLjFDMjAgMTggOS45MDYgMTQgNi45OTcgMTkuODVjLTIuNDk3IDUuNjUgNC44NTMgOSA0Ljg1MyA5IiBzdHJva2U9IiNlY2VjZWMiLz48cGF0aCBkPSJNMTEuNSAzMGM1LjUtMyAxNS41LTMgMjEgMG0tMjEgMy41YzUuNS0zIDE1LjUtMyAyMSAwbS0yMSAzLjVjNS41LTMgMTUuNS0zIDIxIDAiIHN0cm9rZT0iI2VjZWNlYyIvPjwvZz48L3N2Zz4=');
}
var Chessground;(()=>{"use strict";var e={d:(t,o)=>{for(var n in o)e.o(o,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:o[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};(()=>{e.r(t),e.d(t,{Chessground:()=>Ve});const o=["white","black"],n=["a","b","c","d","e","f","g","h"],r=["1","2","3","4","5","6","7","8"],s=[...r].reverse(),i=Array.prototype.concat(...n.map((e=>r.map((t=>e+t))))),c=e=>i[8*e[0]+e[1]],a=e=>[e.charCodeAt(0)-97,e.charCodeAt(1)-49],l=i.map(a),d=()=>{let e;return{start(){e=performance.now()},cancel(){e=void 0},stop(){if(!e)return 0;const t=performance.now()-e;return e=void 0,t}}},u=e=>"white"===e?"black":"white",p=(e,t)=>{const o=e[0]-t[0],n=e[1]-t[1];return o*o+n*n},f=(e,t)=>e.role===t.role&&e.color===t.color,g=e=>(t,o)=>[(o?t[0]:7-t[0])*e.width/8,(o?7-t[1]:t[1])*e.height/8],h=(e,t)=>{e.style.transform=`translate(${t[0]}px,${t[1]}px)`},m=(e,t)=>{e.style.visibility=t?"visible":"hidden"},v=e=>{var t;return e.clientX||0===e.clientX?[e.clientX,e.clientY]:(null===(t=e.targetTouches)||void 0===t?void 0:t[0])?[e.targetTouches[0].clientX,e.targetTouches[0].clientY]:void 0},b=e=>2===e.buttons||2===e.button,w=(e,t)=>{const o=document.createElement(e);return t&&(o.className=t),o};function y(e,t,o){const n=a(e);return t||(n[0]=7-n[0],n[1]=7-n[1]),[o.left+o.width*n[0]/8+o.width/16,o.top+o.height*(7-n[1])/8+o.height/16]}function k(e,t){return Math.abs(e-t)}const C=(e,t,o,n)=>{const r=k(e,o),s=k(t,n);return 1===r&&2===s||2===r&&1===s},M=(e,t,o,n)=>k(e,o)===k(t,n),S=(e,t,o,n)=>e===o||t===n,P=(e,t,o,n)=>M(e,t,o,n)||S(e,t,o,n);function x(e,t,o){const n=e.get(t);if(!n)return[];const r=a(t),s=n.role,i="pawn"===s?(d=n.color,(e,t,o,n)=>k(e,o)<2&&("white"===d?n===t+1||t<=1&&n===t+2&&e===o:n===t-1||t>=6&&n===t-2&&e===o)):"knight"===s?C:"bishop"===s?M:"rook"===s?S:"queen"===s?P:function(e,t,o){return(n,r,s,i)=>k(n,s)<2&&k(r,i)<2||o&&r===i&&r===("white"===e?0:7)&&(4===n&&(2===s&&t.includes(0)||6===s&&t.includes(7))||t.includes(s))}(n.color,function(e,t){const o="white"===t?"1":"8",n=[];for(const[r,s]of e)r[1]===o&&s.color===t&&"rook"===s.role&&n.push(a(r)[0]);return n}(e,n.color),o);var d;return l.filter((e=>(r[0]!==e[0]||r[1]!==e[1])&&i(r[0],r[1],e[0],e[1]))).map(c)}function A(e,...t){e&&setTimeout((()=>e(...t)),1)}function K(e){e.premovable.current&&(e.premovable.current=void 0,A(e.premovable.events.unset))}function T(e){const t=e.predroppable;t.current&&(t.current=void 0,A(t.events.unset))}function q(e,t,o){const n=e.pieces.get(t),r=e.pieces.get(o);if(t===o||!n)return!1;const s=r&&r.color!==n.color?r:void 0;return o===e.selected&&W(e),A(e.events.move,t,o,s),function(e,t,o){if(!e.autoCastle)return!1;const n=e.pieces.get(t);if(!n||"king"!==n.role)return!1;const r=a(t),s=a(o);if(0!==r[1]&&7!==r[1]||r[1]!==s[1])return!1;4!==r[0]||e.pieces.has(o)||(6===s[0]?o=c([7,s[1]]):2===s[0]&&(o=c([0,s[1]])));const i=e.pieces.get(o);return!(!i||i.color!==n.color||"rook"!==i.role||(e.pieces.delete(t),e.pieces.delete(o),r[0]<s[0]?(e.pieces.set(c([6,s[1]]),n),e.pieces.set(c([5,s[1]]),i)):(e.pieces.set(c([2,s[1]]),n),e.pieces.set(c([3,s[1]]),i)),0))}(e,t,o)||(e.pieces.set(o,n),e.pieces.delete(t)),e.lastMove=[t,o],e.check=void 0,A(e.events.change),s||!0}function L(e,t,o,n){if(e.pieces.has(o)){if(!n)return!1;e.pieces.delete(o)}return A(e.events.dropNewPiece,t,o),e.pieces.set(o,t),e.lastMove=[o],e.check=void 0,A(e.events.change),e.movable.dests=void 0,e.turnColor=u(e.turnColor),!0}function O(e,t,o){const n=q(e,t,o);return n&&(e.movable.dests=void 0,e.turnColor=u(e.turnColor),e.animation.current=void 0),n}function N(e,t,o){if(R(e,t,o)){const n=O(e,t,o);if(n){const r=e.hold.stop();W(e);const s={premove:!1,ctrlKey:e.stats.ctrlKey,holdTime:r};return!0!==n&&(s.captured=n),A(e.movable.events.after,t,o,s),!0}}else if(function(e,t,o){return t!==o&&j(e,t)&&x(e.pieces,t,e.premovable.castle).includes(o)}(e,t,o))return function(e,t,o,n){T(e),e.premovable.current=[t,o],A(e.premovable.events.set,t,o,n)}(e,t,o,{ctrlKey:e.stats.ctrlKey}),W(e),!0;return W(e),!1}function D(e,t,o,n){const r=e.pieces.get(t);r&&(function(e,t,o){const n=e.pieces.get(t);return!(!n||t!==o&&e.pieces.has(o)||"both"!==e.movable.color&&(e.movable.color!==n.color||e.turnColor!==n.color))}(e,t,o)||n)?(e.pieces.delete(t),L(e,r,o,n),A(e.movable.events.afterNewPiece,r.role,o,{premove:!1,predrop:!1})):r&&function(e,t,o){const n=e.pieces.get(t),r=e.pieces.get(o);return!!n&&(!r||r.color!==e.movable.color)&&e.predroppable.enabled&&("pawn"!==n.role||"1"!==o[1]&&"8"!==o[1])&&e.movable.color===n.color&&e.turnColor!==n.color}(e,t,o)?function(e,t,o){K(e),e.predroppable.current={role:t,key:o},A(e.predroppable.events.set,t,o)}(e,r.role,o):(K(e),T(e)),e.pieces.delete(t),W(e)}function $(e,t,o){if(A(e.events.select,t),e.selected){if(e.selected===t&&!e.draggable.enabled)return W(e),void e.hold.cancel();if((e.selectable.enabled||o)&&e.selected!==t&&N(e,e.selected,t))return void(e.stats.dragged=!1)}(H(e,t)||j(e,t))&&(E(e,t),e.hold.start())}function E(e,t){e.selected=t,j(e,t)?e.premovable.dests=x(e.pieces,t,e.premovable.castle):e.premovable.dests=void 0}function W(e){e.selected=void 0,e.premovable.dests=void 0,e.hold.cancel()}function H(e,t){const o=e.pieces.get(t);return!!o&&("both"===e.movable.color||e.movable.color===o.color&&e.turnColor===o.color)}function R(e,t,o){var n,r;return t!==o&&H(e,t)&&(e.movable.free||!!(null===(r=null===(n=e.movable.dests)||void 0===n?void 0:n.get(t))||void 0===r?void 0:r.includes(o)))}function j(e,t){const o=e.pieces.get(t);return!!o&&e.premovable.enabled&&e.movable.color===o.color&&e.turnColor!==o.color}function B(e){const t=e.premovable.current;if(!t)return!1;const o=t[0],n=t[1];let r=!1;if(R(e,o,n)){const t=O(e,o,n);if(t){const s={premove:!0};!0!==t&&(s.captured=t),A(e.movable.events.after,o,n,s),r=!0}}return K(e),r}function F(e){K(e),T(e),W(e)}function z(e){e.movable.color=e.movable.dests=e.animation.current=void 0,F(e)}function I(e,t,o){let n=Math.floor(8*(e[0]-o.left)/o.width);t||(n=7-n);let r=7-Math.floor(8*(e[1]-o.top)/o.height);return t||(r=7-r),n>=0&&n<8&&r>=0&&r<8?c([n,r]):void 0}function V(e){return"white"===e.orientation}const G="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",U={p:"pawn",r:"rook",n:"knight",b:"bishop",q:"queen",k:"king"},X={pawn:"p",rook:"r",knight:"n",bishop:"b",queen:"q",king:"k"};function Y(e){"start"===e&&(e=G);const t=new Map;let o=7,n=0;for(const r of e)switch(r){case" ":return t;case"/":if(--o,o<0)return t;n=0;break;case"~":{const e=t.get(c([n,o]));e&&(e.promoted=!0);break}default:{const e=r.charCodeAt(0);if(e<57)n+=e-48;else{const e=r.toLowerCase();t.set(c([n,o]),{role:U[e],color:r===e?"black":"white"}),++n}}}return t}function Z(e,t){t.animation&&(_(e.animation,t.animation),(e.animation.duration||0)<70&&(e.animation.enabled=!1))}function Q(e,t){var o,n;if((null===(o=t.movable)||void 0===o?void 0:o.dests)&&(e.movable.dests=void 0),(null===(n=t.drawable)||void 0===n?void 0:n.autoShapes)&&(e.drawable.autoShapes=[]),_(e,t),t.fen&&(e.pieces=Y(t.fen),e.drawable.shapes=[]),"check"in t&&function(e,t){if(e.check=void 0,!0===t&&(t=e.turnColor),t)for(const[o,n]of e.pieces)"king"===n.role&&n.color===t&&(e.check=o)}(e,t.check||!1),"lastMove"in t&&!t.lastMove?e.lastMove=void 0:t.lastMove&&(e.lastMove=t.lastMove),e.selected&&E(e,e.selected),Z(e,t),!e.movable.rookCastle&&e.movable.dests){const t="white"===e.movable.color?"1":"8",o="e"+t,n=e.movable.dests.get(o),r=e.pieces.get(o);if(!n||!r||"king"!==r.role)return;e.movable.dests.set(o,n.filter((e=>!(e==="a"+t&&n.includes("c"+t)||e==="h"+t&&n.includes("g"+t)))))}}function _(e,t){for(const o in t)J(e[o])&&J(t[o])?_(e[o],t[o]):e[o]=t[o]}function J(e){return"object"==typeof e}function ee(e,t){return t.animation.enabled?function(e,t){const o=new Map(t.pieces),n=e(t),r=function(e,t){const o=new Map,n=[],r=new Map,s=[],c=[],a=new Map;let l,d,u;for(const[t,o]of e)a.set(t,oe(t,o));for(const e of i)l=t.pieces.get(e),d=a.get(e),l?d?f(l,d.piece)||(s.push(d),c.push(oe(e,l))):c.push(oe(e,l)):d&&s.push(d);for(const e of c)d=ne(e,s.filter((t=>f(e.piece,t.piece)))),d&&(u=[d.pos[0]-e.pos[0],d.pos[1]-e.pos[1]],o.set(e.key,u.concat(u)),n.push(d.key));for(const e of s)n.includes(e.key)||r.set(e.key,e.piece);return{anims:o,fadings:r}}(o,t);if(r.anims.size||r.fadings.size){const e=t.animation.current&&t.animation.current.start;t.animation.current={start:performance.now(),frequency:1/t.animation.duration,plan:r},e||re(t,performance.now())}else t.dom.redraw();return n}(e,t):te(e,t)}function te(e,t){const o=e(t);return t.dom.redraw(),o}function oe(e,t){return{key:e,pos:a(e),piece:t}}function ne(e,t){return t.sort(((t,o)=>p(e.pos,t.pos)-p(e.pos,o.pos)))[0]}function re(e,t){const o=e.animation.current;if(void 0===o)return void(e.dom.destroyed||e.dom.redrawNow());const n=1-(t-o.start)*o.frequency;if(n<=0)e.animation.current=void 0,e.dom.redrawNow();else{const t=(r=n)<.5?4*r*r*r:(r-1)*(2*r-2)*(2*r-2)+1;for(const e of o.plan.anims.values())e[2]=e[0]*t,e[3]=e[1]*t;e.dom.redrawNow(!0),requestAnimationFrame(((t=performance.now())=>re(e,t)))}var r}const se=["green","red","blue","yellow"];function ie(e,t){if(t.touches&&t.touches.length>1)return;t.stopPropagation(),t.preventDefault(),t.ctrlKey?W(e):F(e);const o=v(t),n=I(o,V(e),e.dom.bounds());n&&(e.drawable.current={orig:n,pos:o,brush:ue(t),snapToValidMove:e.drawable.defaultSnapToValidMove},ce(e))}function ce(e){requestAnimationFrame((()=>{const t=e.drawable.current;if(t){const o=I(t.pos,V(e),e.dom.bounds());o||(t.snapToValidMove=!1);const n=t.snapToValidMove?function(e,t,o,n){const r=a(e),s=l.filter((e=>P(r[0],r[1],e[0],e[1])||C(r[0],r[1],e[0],e[1]))),i=s.map((e=>y(c(e),o,n))).map((e=>p(t,e))),[,d]=i.reduce(((e,t,o)=>e[0]<t?e:[t,o]),[i[0],0]);return c(s[d])}(t.orig,t.pos,V(e),e.dom.bounds()):o;n!==t.mouseSq&&(t.mouseSq=n,t.dest=n!==t.orig?n:void 0,e.dom.redrawNow()),ce(e)}}))}function ae(e,t){e.drawable.current&&(e.drawable.current.pos=v(t))}function le(e){const t=e.drawable.current;t&&(t.mouseSq&&function(e,t){const o=e=>e.orig===t.orig&&e.dest===t.dest,n=e.shapes.find(o);n&&(e.shapes=e.shapes.filter((e=>!o(e)))),n&&n.brush===t.brush||e.shapes.push(t),pe(e)}(e.drawable,t),de(e))}function de(e){e.drawable.current&&(e.drawable.current=void 0,e.dom.redraw())}function ue(e){var t;const o=(e.shiftKey||e.ctrlKey)&&b(e),n=e.altKey||e.metaKey||(null===(t=e.getModifierState)||void 0===t?void 0:t.call(e,"AltGraph"));return se[(o?1:0)+(n?2:0)]}function pe(e){e.onChange&&e.onChange(e.shapes)}function fe(e){requestAnimationFrame((()=>{var t;const o=e.draggable.current;if(!o)return;(null===(t=e.animation.current)||void 0===t?void 0:t.plan.anims.has(o.orig))&&(e.animation.current=void 0);const n=e.pieces.get(o.orig);if(n&&f(n,o.piece)){if(!o.started&&p(o.pos,o.origPos)>=Math.pow(e.draggable.distance,2)&&(o.started=!0),o.started){if("function"==typeof o.element){const e=o.element();if(!e)return;e.cgDragging=!0,e.classList.add("dragging"),o.element=e}const t=e.dom.bounds();h(o.element,[o.pos[0]-t.left-t.width/16,o.pos[1]-t.top-t.height/16]),o.keyHasChanged||(o.keyHasChanged=o.orig!==I(o.pos,V(e),t))}}else me(e);fe(e)}))}function ge(e,t){e.draggable.current&&(!t.touches||t.touches.length<2)&&(e.draggable.current.pos=v(t))}function he(e,t){const o=e.draggable.current;if(!o)return;if("touchend"===t.type&&!1!==t.cancelable&&t.preventDefault(),"touchend"===t.type&&o.originTarget!==t.target&&!o.newPiece)return void(e.draggable.current=void 0);K(e),T(e);const n=I(v(t)||o.pos,V(e),e.dom.bounds());n&&o.started&&o.orig!==n?o.newPiece?D(e,o.orig,n,o.force):(e.stats.ctrlKey=t.ctrlKey,N(e,o.orig,n)&&(e.stats.dragged=!0)):o.newPiece?e.pieces.delete(o.orig):e.draggable.deleteOnDropOff&&!n&&(e.pieces.delete(o.orig),A(e.events.change)),(o.orig!==o.previouslySelected&&!o.keyHasChanged||o.orig!==n&&n)&&e.selectable.enabled||W(e),ve(e),e.draggable.current=void 0,e.dom.redraw()}function me(e){const t=e.draggable.current;t&&(t.newPiece&&e.pieces.delete(t.orig),e.draggable.current=void 0,W(e),ve(e),e.dom.redraw())}function ve(e){const t=e.dom.elements;t.ghost&&m(t.ghost,!1)}function be(e,t){let o=e.dom.elements.board.firstChild;for(;o;){if(o.cgKey===t&&"PIECE"===o.tagName)return o;o=o.nextSibling}}function we(e,t){e.exploding&&(t?e.exploding.stage=t:e.exploding=void 0,e.dom.redraw())}function ye(e){return document.createElementNS("http://www.w3.org/2000/svg",e)}function ke(e,t,o,n,r){const s=e.dom.bounds(),i=new Map,c=[];for(const e of t)i.set(e.hash,!1);let a,l=r.firstChild;for(;l;)a=l.getAttribute("cgHash"),i.has(a)?i.set(a,!0):c.push(l),l=l.nextSibling;for(const e of c)r.removeChild(e);for(const c of t)i.get(c.hash)||r.appendChild(Pe(e,c,o,n,s))}function Ce({orig:e,dest:t,brush:o,piece:n,modifiers:r,customSvg:s},i,c,a){return[a.width,a.height,c,e,t,o,t&&(i.get(t)||0)>1,n&&Me(n),r&&(l=r,""+(l.lineWidth||"")),s&&Se(s)].filter((e=>e)).join(",");var l}function Me(e){return[e.color,e.role,e.scale].filter((e=>e)).join(",")}function Se(e){let t=0;for(let o=0;o<e.length;o++)t=(t<<5)-t+e.charCodeAt(o)>>>0;return"custom-"+t.toString()}function Pe(e,{shape:t,current:o,hash:n},r,s,i){let c;if(t.customSvg){const o=Ke(a(t.orig),e.orientation);c=function(e,t,o){const[n,r]=Oe(t,o),s=Ae(ye("g"),{transform:`translate(${n},${r})`}),i=Ae(ye("svg"),{width:1,height:1,viewBox:"0 0 100 100"});return s.appendChild(i),i.innerHTML=e,s}(t.customSvg,o,i)}else if(t.piece)c=function(e,t,o,n){const r=Oe(t,n),s=o.color[0]+("knight"===o.role?"n":o.role[0]).toUpperCase();return Ae(ye("image"),{className:`${o.role} ${o.color}`,x:r[0]-.5,y:r[1]-.5,width:1,height:1,href:e+s+".svg",transform:`scale(${o.scale||1})`,"transform-origin":`${r[0]} ${r[1]}`})}(e.drawable.pieces.baseUrl,Ke(a(t.orig),e.orientation),t.piece,i);else{const n=Ke(a(t.orig),e.orientation);if(t.dest){let l=r[t.brush];t.modifiers&&(l=Te(l,t.modifiers)),c=function(e,t,o,n,r,s){const i=function(e){return(e?20:10)/64}(r&&!n),c=Oe(t,s),a=Oe(o,s),l=a[0]-c[0],d=a[1]-c[1],u=Math.atan2(d,l),p=Math.cos(u)*i,f=Math.sin(u)*i;return Ae(ye("line"),{stroke:e.color,"stroke-width":qe(e,n),"stroke-linecap":"round","marker-end":"url(#arrowhead-"+e.key+")",opacity:Le(e,n),x1:c[0],y1:c[1],x2:a[0]-p,y2:a[1]-f})}(l,n,Ke(a(t.dest),e.orientation),o,(s.get(t.dest)||0)>1,i)}else c=function(e,t,o,n){const r=Oe(t,n),s=[3/64,4/64],i=(n.width+n.height)/(4*Math.max(n.width,n.height));return Ae(ye("circle"),{stroke:e.color,"stroke-width":s[o?0:1],fill:"none",opacity:Le(e,o),cx:r[0],cy:r[1],r:i-s[1]/2})}(r[t.brush],n,o,i)}return c.setAttribute("cgHash",n),c}function xe(e){const t=Ae(ye("marker"),{id:"arrowhead-"+e.key,orient:"auto",markerWidth:4,markerHeight:8,refX:2.05,refY:2.01});return t.appendChild(Ae(ye("path"),{d:"M0,0 V4 L3,2 Z",fill:e.color})),t.setAttribute("cgKey",e.key),t}function Ae(e,t){for(const o in t)e.setAttribute(o,t[o]);return e}function Ke(e,t){return"white"===t?e:[7-e[0],7-e[1]]}function Te(e,t){return{color:e.color,opacity:Math.round(10*e.opacity)/10,lineWidth:Math.round(t.lineWidth||e.lineWidth),key:[e.key,t.lineWidth].filter((e=>e)).join("")}}function qe(e,t){return(e.lineWidth||10)*(t?.85:1)/64}function Le(e,t){return(e.opacity||1)*(t?.9:1)}function Oe(e,t){const o=Math.min(1,t.width/t.height),n=Math.min(1,t.height/t.width);return[(e[0]-3.5)*o,(3.5-e[1])*n]}function Ne(e,t){const o=w("coords",t);let n;for(const t of e)n=w("coord"),n.textContent=t,o.appendChild(n);return o}function De(e,t){const o=e.dom.elements.board;if("ResizeObserver"in window&&new ResizeObserver(t).observe(e.dom.elements.wrap),e.viewOnly)return;const n=function(e){return t=>{e.draggable.current?me(e):e.drawable.current?de(e):t.shiftKey||b(t)?e.drawable.enabled&&ie(e,t):e.viewOnly||(e.dropmode.active?function(e,t){if(!e.dropmode.active)return;K(e),T(e);const o=e.dropmode.piece;if(o){e.pieces.set("a0",o);const n=v(t),r=n&&I(n,V(e),e.dom.bounds());r&&D(e,"a0",r)}e.dom.redraw()}(e,t):function(e,t){if(!t.isTrusted||void 0!==t.button&&0!==t.button)return;if(t.touches&&t.touches.length>1)return;const o=e.dom.bounds(),n=v(t),r=I(n,V(e),o);if(!r)return;const s=e.pieces.get(r),i=e.selected;var c;i||!e.drawable.enabled||!e.drawable.eraseOnClick&&s&&s.color===e.turnColor||(c=e).drawable.shapes.length&&(c.drawable.shapes=[],c.dom.redraw(),pe(c.drawable)),!1!==t.cancelable&&(!t.touches||e.blockTouchScroll||s||i||function(e,t){const o=V(e),n=e.dom.bounds(),r=Math.pow(n.width/8,2);for(const s of e.pieces.keys()){const e=y(s,o,n);if(p(e,t)<=r)return!0}return!1}(e,n))&&t.preventDefault();const l=!!e.premovable.current,d=!!e.predroppable.current;e.stats.ctrlKey=t.ctrlKey,e.selected&&R(e,e.selected,r)?ee((e=>$(e,r)),e):$(e,r);const u=e.selected===r,f=be(e,r);if(s&&f&&u&&function(e,t){const o=e.pieces.get(t);return!!o&&e.draggable.enabled&&("both"===e.movable.color||e.movable.color===o.color&&(e.turnColor===o.color||e.premovable.enabled))}(e,r)){e.draggable.current={orig:r,piece:s,origPos:n,pos:n,started:e.draggable.autoDistance&&e.stats.dragged,element:f,previouslySelected:i,originTarget:t.target,keyHasChanged:!1},f.cgDragging=!0,f.classList.add("dragging");const c=e.dom.elements.ghost;c&&(c.className=`ghost ${s.color} ${s.role}`,h(c,g(o)(a(r),V(e))),m(c,!0)),fe(e)}else l&&K(e),d&&T(e);e.dom.redraw()}(e,t))}}(e);o.addEventListener("touchstart",n,{passive:!1}),o.addEventListener("mousedown",n,{passive:!1}),(e.disableContextMenu||e.drawable.enabled)&&o.addEventListener("contextmenu",(e=>e.preventDefault()))}function $e(e,t,o,n){return e.addEventListener(t,o,n),()=>e.removeEventListener(t,o,n)}function Ee(e,t,o){return n=>{e.drawable.current?e.drawable.enabled&&o(e,n):e.viewOnly||t(e,n)}}function We(e){const t=e.dom.elements.wrap.getBoundingClientRect(),o=e.dom.elements.container,n=t.height/t.width,r=8*Math.floor(t.width*window.devicePixelRatio/8)/window.devicePixelRatio,s=r*n;o.style.width=r+"px",o.style.height=s+"px",e.dom.bounds.clear(),e.addDimensionsCssVars&&(document.documentElement.style.setProperty("--cg-width",r+"px"),document.documentElement.style.setProperty("--cg-height",s+"px"))}function He(e){return"PIECE"===e.tagName}function Re(e){return"SQUARE"===e.tagName}function je(e,t){for(const o of t)e.dom.elements.board.removeChild(o)}function Be(e,t){const o=e[1];return`${t?10-o:3+o}`}function Fe(e){return`${e.color} ${e.role}`}function ze(e,t,o){const n=e.get(t);n?e.set(t,`${n} ${o}`):e.set(t,o)}function Ie(e,t,o){const n=e.get(t);n?n.push(o):e.set(t,[o])}function Ve(e,t){const i={pieces:Y(G),orientation:"white",turnColor:"white",coordinates:!0,autoCastle:!0,viewOnly:!1,disableContextMenu:!1,addPieceZIndex:!1,addDimensionsCssVars:!1,blockTouchScroll:!1,pieceKey:!1,highlight:{lastMove:!0,check:!0},animation:{enabled:!0,duration:200},movable:{free:!0,color:"both",showDests:!0,events:{},rookCastle:!0},premovable:{enabled:!0,showDests:!0,castle:!0,events:{}},predroppable:{enabled:!1,events:{}},draggable:{enabled:!0,distance:3,autoDistance:!0,showGhost:!0,deleteOnDropOff:!1},dropmode:{active:!1},selectable:{enabled:!0},stats:{dragged:!("ontouchstart"in window)},events:{},drawable:{enabled:!0,visible:!0,defaultSnapToValidMove:!0,eraseOnClick:!0,shapes:[],autoShapes:[],brushes:{green:{key:"g",color:"#15781B",opacity:1,lineWidth:10},red:{key:"r",color:"#882020",opacity:1,lineWidth:10},blue:{key:"b",color:"#003088",opacity:1,lineWidth:10},yellow:{key:"y",color:"#e68f00",opacity:1,lineWidth:10},paleBlue:{key:"pb",color:"#003088",opacity:.4,lineWidth:15},paleGreen:{key:"pg",color:"#15781B",opacity:.4,lineWidth:15},paleRed:{key:"pr",color:"#882020",opacity:.4,lineWidth:15},paleGrey:{key:"pgr",color:"#4a4a4a",opacity:.35,lineWidth:15}},pieces:{baseUrl:"https://lichess1.org/assets/piece/cburnett/"},prevSvgHash:""},hold:d()};function c(){const t="dom"in i?i.dom.unbind:void 0,s=function(e,t){e.innerHTML="",e.classList.add("cg-wrap");for(const n of o)e.classList.toggle("orientation-"+n,t.orientation===n);e.classList.toggle("manipulable",!t.viewOnly);const s=w("cg-container");e.appendChild(s);const i=w("cg-board");let c,a,l;if(s.appendChild(i),t.drawable.visible&&(c=Ae(ye("svg"),{class:"cg-shapes",viewBox:"-4 -4 8 8",preserveAspectRatio:"xMidYMid slice"}),c.appendChild(ye("defs")),c.appendChild(ye("g")),a=Ae(ye("svg"),{class:"cg-custom-svgs",viewBox:"-3.5 -3.5 8 8",preserveAspectRatio:"xMidYMid slice"}),a.appendChild(ye("g")),s.appendChild(c),s.appendChild(a)),t.coordinates){const e="black"===t.orientation?" black":"";s.appendChild(Ne(r,"ranks"+e)),s.appendChild(Ne(n,"files"+e))}return t.draggable.showGhost&&(l=w("piece","ghost"),m(l,!1),s.appendChild(l)),{board:i,container:s,wrap:e,ghost:l,svg:c,customSvg:a}}(e,i),c=function(e){let t;const o=()=>(void 0===t&&(t=s.board.getBoundingClientRect()),t);return o.clear=()=>{t=void 0},o}(),l=e=>{(function(e){const t=V(e),o=g(e.dom.bounds()),n=e.dom.elements.board,r=e.pieces,s=e.animation.current,i=s?s.plan.anims:new Map,c=s?s.plan.fadings:new Map,l=e.draggable.current,d=function(e){var t;const o=new Map;if(e.lastMove&&e.highlight.lastMove)for(const t of e.lastMove)ze(o,t,"last-move");if(e.check&&e.highlight.check&&ze(o,e.check,"check"),e.selected&&(ze(o,e.selected,"selected"),e.movable.showDests)){const n=null===(t=e.movable.dests)||void 0===t?void 0:t.get(e.selected);if(n)for(const t of n)ze(o,t,"move-dest"+(e.pieces.has(t)?" oc":""));const r=e.premovable.dests;if(r)for(const t of r)ze(o,t,"premove-dest"+(e.pieces.has(t)?" oc":""))}const n=e.premovable.current;if(n)for(const e of n)ze(o,e,"current-premove");else e.predroppable.current&&ze(o,e.predroppable.current.key,"current-premove");const r=e.exploding;if(r)for(const e of r.keys)ze(o,e,"exploding"+r.stage);return o}(e),u=new Set,p=new Set,f=new Map,m=new Map;let v,b,y,k,C,M,S,P,x,A;for(b=n.firstChild;b;){if(v=b.cgKey,He(b))if(y=r.get(v),C=i.get(v),M=c.get(v),k=b.cgPiece,!b.cgDragging||l&&l.orig===v||(b.classList.remove("dragging"),h(b,o(a(v),t)),b.cgDragging=!1),!M&&b.cgFading&&(b.cgFading=!1,b.classList.remove("fading")),y){if(C&&b.cgAnimating&&k===Fe(y)){const e=a(v);e[0]+=C[2],e[1]+=C[3],b.classList.add("anim"),h(b,o(e,t))}else b.cgAnimating&&(b.cgAnimating=!1,b.classList.remove("anim"),h(b,o(a(v),t)),e.addPieceZIndex&&(b.style.zIndex=Be(a(v),t)));k!==Fe(y)||M&&b.cgFading?M&&k===Fe(M)?(b.classList.add("fading"),b.cgFading=!0):Ie(f,k,b):u.add(v)}else Ie(f,k,b);else if(Re(b)){const e=b.className;d.get(v)===e?p.add(v):Ie(m,e,b)}b=b.nextSibling}for(const[e,r]of d)if(!p.has(e)){x=m.get(r),A=x&&x.pop();const s=o(a(e),t);if(A)A.cgKey=e,h(A,s);else{const t=w("square",r);t.cgKey=e,h(t,s),n.insertBefore(t,n.firstChild)}}for(const[s,c]of r)if(C=i.get(s),!u.has(s))if(S=f.get(Fe(c)),P=S&&S.pop(),P){P.cgKey=s,P.cgFading&&(P.classList.remove("fading"),P.cgFading=!1);const n=a(s);e.addPieceZIndex&&(P.style.zIndex=Be(n,t)),C&&(P.cgAnimating=!0,P.classList.add("anim"),n[0]+=C[2],n[1]+=C[3]),h(P,o(n,t))}else{const r=Fe(c),i=w("piece",r),l=a(s);i.cgPiece=r,i.cgKey=s,C&&(i.cgAnimating=!0,l[0]+=C[2],l[1]+=C[3]),h(i,o(l,t)),e.addPieceZIndex&&(i.style.zIndex=Be(l,t)),n.appendChild(i)}for(const t of f.values())je(e,t);for(const t of m.values())je(e,t)})(u),!e&&s.svg&&function(e,t,o){const n=e.drawable,r=n.current,s=r&&r.mouseSq?r:void 0,i=new Map,c=e.dom.bounds();for(const e of n.shapes.concat(n.autoShapes).concat(s?[s]:[]))e.dest&&i.set(e.dest,(i.get(e.dest)||0)+1);const a=n.shapes.concat(n.autoShapes).map((e=>({shape:e,current:!1,hash:Ce(e,i,!1,c)})));s&&a.push({shape:s,current:!0,hash:Ce(s,i,!0,c)});const l=a.map((e=>e.hash)).join(";");if(l===e.drawable.prevSvgHash)return;e.drawable.prevSvgHash=l;const d=t.querySelector("defs"),u=t.querySelector("g"),p=o.querySelector("g");!function(e,t,o){const n=new Map;let r;for(const o of t)o.shape.dest&&(r=e.brushes[o.shape.brush],o.shape.modifiers&&(r=Te(r,o.shape.modifiers)),n.set(r.key,r));const s=new Set;let i=o.firstChild;for(;i;)s.add(i.getAttribute("cgKey")),i=i.nextSibling;for(const[e,t]of n.entries())s.has(e)||o.appendChild(xe(t))}(n,a,d),ke(e,a.filter((e=>!e.shape.customSvg)),n.brushes,i,u),ke(e,a.filter((e=>e.shape.customSvg)),n.brushes,i,p)}(u,s.svg,s.customSvg)},d=()=>{We(u),function(e){const t=V(e),o=g(e.dom.bounds());let n=e.dom.elements.board.firstChild;for(;n;)(He(n)&&!n.cgAnimating||Re(n))&&h(n,o(a(n.cgKey),t)),n=n.nextSibling}(u)},u=i;return u.dom={elements:s,bounds:c,redraw:Ge(l),redrawNow:l,unbind:t},u.drawable.prevSvgHash="",We(u),l(!1),De(u,d),t||(u.dom.unbind=function(e,t){const o=[];if("ResizeObserver"in window||o.push($e(document.body,"chessground.resize",t)),!e.viewOnly){const t=Ee(e,ge,ae),n=Ee(e,he,le);for(const e of["touchmove","mousemove"])o.push($e(document,e,t));for(const e of["touchend","mouseup"])o.push($e(document,e,n));const r=()=>e.dom.bounds.clear();o.push($e(document,"scroll",r,{capture:!0,passive:!0})),o.push($e(window,"resize",r,{passive:!0}))}return()=>o.forEach((e=>e()))}(u,d)),u.events.insert&&u.events.insert(s),u}return Q(i,t||{}),function(e,t){function o(){!function(e){e.orientation=u(e.orientation),e.animation.current=e.draggable.current=e.selected=void 0}(e),t()}return{set(t){t.orientation&&t.orientation!==e.orientation&&o(),Z(e,t),(t.fen?ee:te)((e=>Q(e,t)),e)},state:e,getFen:()=>{return t=e.pieces,s.map((e=>n.map((o=>{const n=t.get(o+e);if(n){const e=X[n.role];return"white"===n.color?e.toUpperCase():e}return"1"})).join(""))).join("/").replace(/1{2,}/g,(e=>e.length.toString()));var t},toggleOrientation:o,setPieces(t){ee((e=>function(e,t){for(const[o,n]of t)n?e.pieces.set(o,n):e.pieces.delete(o)}(e,t)),e)},selectSquare(t,o){t?ee((e=>$(e,t,o)),e):e.selected&&(W(e),e.dom.redraw())},move(t,o){ee((e=>q(e,t,o)),e)},newPiece(t,o){ee((e=>L(e,t,o)),e)},playPremove(){if(e.premovable.current){if(ee(B,e))return!0;e.dom.redraw()}return!1},playPredrop(t){if(e.predroppable.current){const o=function(e,t){const o=e.predroppable.current;let n=!1;return!!o&&(t(o)&&L(e,{role:o.role,color:e.movable.color},o.key)&&(A(e.movable.events.afterNewPiece,o.role,o.key,{premove:!1,predrop:!0}),n=!0),T(e),n)}(e,t);return e.dom.redraw(),o}return!1},cancelPremove(){te(K,e)},cancelPredrop(){te(T,e)},cancelMove(){te((e=>{F(e),me(e)}),e)},stop(){te((e=>{z(e),me(e)}),e)},explode(t){!function(e,t){e.exploding={stage:1,keys:t},e.dom.redraw(),setTimeout((()=>{we(e,2),setTimeout((()=>we(e,void 0)),120)}),120)}(e,t)},setAutoShapes(t){te((e=>e.drawable.autoShapes=t),e)},setShapes(t){te((e=>e.drawable.shapes=t),e)},getKeyAtDomPos:t=>I(t,V(e),e.dom.bounds()),redrawAll:t,dragNewPiece(t,o,n){!function(e,t,o,n){const r="a0";e.pieces.set(r,t),e.dom.redraw();const s=v(o);e.draggable.current={orig:r,piece:t,origPos:s,pos:s,started:!0,element:()=>be(e,r),originTarget:o.target,newPiece:!0,force:!!n,keyHasChanged:!1},fe(e)}(e,t,o,n)},destroy(){z(e),e.dom.unbind&&e.dom.unbind(),e.dom.destroyed=!0}}}(c(),c)}function Ge(e){let t=!1;return()=>{t||(t=!0,requestAnimationFrame((()=>{e(),t=!1})))}}})(),Chessground=t})();
<!doctype html>
<html>
<head>
<title>The Knight's Voyage</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="chessground.css">
<style type="text/css">
.timer {
text-align: center;
font-size: 80px;
}
.board-container-side-by-side {
display: flex;
}
#board-column { margin-pottom: 20px; }
body {
background-color: black;
color: white;
}
table {
background-color: #262421;
}
tr {
border: none;
}
p { text-align: center; font-size: 20px; }
td, th {
border: none;
text-align: center;
padding-right: 20px;
padding-left: 20px;
}
.splits-pb { color: rgb(22, 166, 255); }
.splits-ahead.splits-gain { color: rgb(41, 204, 84); }
.splits-ahead.splits-loss { color: rgb(112, 204, 137); }
.splits-behind.splits-gain { color: rgb(204, 120, 112); }
.splits-behind.splits-loss { color: rgb(204, 55, 41); }
.splits-glod { color: rgb(216, 175, 31); }
table { margin: auto; font-size: 30px; }
</style>
</head>
<script src="chessground.js"></script>
<script src="chess.js"></script>
<body>
<div id="board-container">
<div id="board-column">
<div id="board" style="width: 900px; height: 900px; margin: auto"></div>
</div>
<div id="splits">
<table>
<thead>
<tr>
<th>Square</th>
<th>Time</th>
<th></th>
<th>Best</th>
<th># Moves</th>
<th>Min. #Moves</th>
</tr>
</thead>
<tbody>
<tr id="splits-total">
<td class="splits-square">Total</td>
<td class="splits-time"></td>
<td class="splits-delta"></td>
<td class="splits-best-time"></td>
<td class="splits-num-moves"></td>
<td class="splits-min-moves"></td>
</tr>
<tr id="splits-best">
<td class="splits-square">Sum of Best</td>
<td class="splits-time"></td>
<td class="splits-delta"></td>
<td class="splits-best-time"></td>
<td class="splits-num-moves"></td>
<td class="splits-min-moves"></td>
</tr>
</tbody>
</table>
</div>
</div>
<p class="timer">Time: <span id="timer-count">0.00</span></p>
<p>
<label for="fen">FEN:</label>
<input type="text" id="fen" name="fen" value="">
<button id="load-fen">Load</button>
<br/>
<label for="fen-selector">Preset: </label>
<select name="fen-selector" id="fen-selector">
<option value="7n/8/8/3Q4/8/8/8/8 b - - 0 1">One Queen</option>
<option value="7n/8/8/8/8/8/8/8 b - - 0 1">Clear Board</option>
<option value="7n/8/8/3B4/8/8/5B2/8 b - - 0 1">Two Bishops</option>
<option value="7n/8/8/1B6/5R2/8/8/8 b - - 0 1">Bishop & Rook</option>
<option value="7n/8/2N5/3N4/8/8/8/3B4 b - - 0 1">Two Knights & Bishop</option>
<option value="5n2/6R1/8/8/8/2R5/8/8 b - - 0 1">Two Rooks</option>
</select>
<br/>
<input type="checkbox" id="show-inaccessible" name="show-inaccessible"
value="on">
<label for="show-inaccessible">Show Inaccessible squares</label>
<br/>
</p>
</body>
<script>
function formatTime(delta) {
return (delta / 1000).toFixed(2).toString();
}
function updateTimer(timestamp) {
if (!startTime) {
startTime = timestamp;
lastSplitTime = timestamp;
}
document.querySelector("#timer-count").innerHTML = formatTime(timestamp - startTime);
if (targetId < targets.length) {
updateSplits(targets[targetId]);
timerUpdateRequest = requestAnimationFrame(updateTimer);
}
}
function isAttacked(game, target) {
let moves = game.moves({legal: false, verbose: true});
return moves.some(move => move.to == target);
}
function passTurn(game) {
let parts = game.fen().split(" ");
parts[1] = parts[1] == "w" ? "b" : "w";
let newFen = parts.join(" ");
game.load(newFen);
}
function successors(game) {
let successors = [];
let moves = game.moves({legal: false, verbose: true});
moves.forEach((move) => {
if (game.get(move.to)) return;
let old_source = game.get(move.from);
let old_target = game.get(move.to);
game.put(old_source, move.to);
game.remove(move.from);
passTurn(game);
if (!isAttacked(game, move.to)) {
passTurn(game);
successors.push([move.from, move.to, game.fen()])
passTurn(game);
}
if (old_target)
game.put(old_target, move.to);
else
game.remove(move.to);
if (old_source)
game.put(old_source, move.from);
else
game.remove(move.from);
passTurn(game);
});
return successors;
}
function legalMoves(game) {
let moves = new Map();
successors(game).forEach((entry) => {
let [previous, square, fen] = entry;
if (!moves.has(previous))
moves.set(previous, [square]);
else
moves.get(previous).push(square);
});
return moves;
}
function updateLegalMoves() {
board.set({
turnColor: game.turn() == game.WHITE ? "white" : "black",
movable: {dests: legalMoves(game)},
});
}
function currentSquare(game) {
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
let file = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][y];
let rank = (x + 1).toString();
let square = file + rank;
if (game.get(square) && game.get(square).color == game.turn())
return square;
}
}
return null;
}
function reachableSquares() {
let square = currentSquare(game);
let reachable = {};
let stack = [[square, game.fen()]];
while (stack.length != 0) {
let [square, fen] = stack.pop();
if (reachable[square]) continue;
reachable[square] = [];
successors(Chess(fen)).forEach((entry) => {
let [previous, nextSquare, fen] = entry;
reachable[square].push(nextSquare);
if (!reachable[nextSquare])
stack.push([nextSquare, fen]);
});
}
return reachable;
}
function graphDistance(graph, source, dest) {
let queue = [[source, 0]];
let i = 0;
let seen = {};
seen[source] = true;
while (i < queue.length) {
let [node, dist] = queue[i++];
if (node == dest)
return dist;
graph[node].forEach(succ => {
if (!seen[succ]) {
queue.push([succ, dist + 1]);
seen[succ] = true;
}
});
}
}
function targetSquares() {
let reachable = reachableSquares();
let targets = [];
for (let x = 0; x < 8; x++) {
for (let y = 0; y < 8; y++) {
let file = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][8 - y - 1];
let rank = ((8 - x - 1) + 1).toString();
let square = file + rank;
if (reachable[square])
targets.push(square);
}
}
return targets;
}
function distances() {
let reachable = reachableSquares();
let targets = targetSquares();
let current = currentSquare(game);
let out = [];
targets.forEach(target => {
out.push(graphDistance(reachable, current, target));
current = target;
});
return out;
}
function createSplits() {
let numMoves = distances();
let table = document.querySelector("#splits tbody")
let summaryRow = document.querySelector("#splits-total")
let bestRow = document.querySelector("#splits-best")
let moveSum = 0;
let sumOfBest = 0;
let bestRun = bestSplits["bestRun"] || {};
let bestTime = 0;
targetSquares().forEach((square, i) => {
let row = document.createElement("tr");
row.id = "splits-" + square;
let squareNode = document.createElement("td");
squareNode.classList.add("splits-square");
squareNode.appendChild(document.createTextNode(square));
row.appendChild(squareNode);
let timeNode = document.createElement("td");
timeNode.classList.add("splits-time");
row.appendChild(timeNode);
let deltaNode = document.createElement("td");
deltaNode.classList.add("splits-delta");
row.appendChild(deltaNode);
let bestTimeNode = document.createElement("td");
bestTimeNode.classList.add("splits-best-time");
row.appendChild(bestTimeNode);
if (square in bestRun) {
bestTime += bestRun[square];
bestTimeNode.appendChild(document.createTextNode(formatTime(bestTime)));
}
if (square in bestSplits) {
if (sumOfBest != null) sumOfBest += bestSplits[square];
}
else
sumOfBest = null;
let numMovesNode = document.createElement("td");
numMovesNode.classList.add("splits-num-moves");
row.appendChild(numMovesNode);
let minMovesNode = document.createElement("td");
minMovesNode.appendChild(document.createTextNode(numMoves[i]));
minMovesNode.classList.add("splits-min-moves");
row.appendChild(minMovesNode);
moveSum += numMoves[i];
table.insertBefore(row, summaryRow);
});
document.querySelector("#splits-total .splits-min-moves").innerHTML = moveSum;
if ("total" in bestSplits) {
summaryRow.querySelector(".splits-best-time").innerHTML =
formatTime(bestSplits["total"]);
}
if (sumOfBest != null) {
bestRow.querySelector(".splits-best-time").innerHTML =
formatTime(sumOfBest);
}
}
function clearSplits() {
const table = document.querySelector("#splits tbody")
targets.forEach(square => {
table.removeChild(document.querySelector("#splits-" + square));
});
let summaryRow = document.querySelector("#splits-total");
let bestRow = document.querySelector("#splits-best");
summaryRow.querySelector(".splits-time").innerHTML = "";
summaryRow.querySelector(".splits-time").classList.remove("splits-pb");
summaryRow.querySelector(".splits-delta").innerHTML = "";
summaryRow.querySelector(".splits-best-time").innerHTML = "";
summaryRow.querySelector(".splits-num-moves").innerHTML = "";
summaryRow.querySelector(".splits-min-moves").innerHTML = "";
bestRow.querySelector(".splits-time").innerHTML = "";
bestRow.querySelector(".splits-delta").innerHTML = "";
bestRow.querySelector(".splits-best-time").innerHTML = "";
bestRow.querySelector(".splits-num-moves").innerHTML = "";
bestRow.querySelector(".splits-min-moves").innerHTML = "";
}
function bestRunTime(target) {
let sum = 0;
for (let i = 0; i < targets.length; i++) {
if (!(targets[i] in bestSplits["bestRun"]))
return null;
sum += bestSplits["bestRun"][targets[i]];
if (targets[i] == target)
return sum;
}
}
function updateSplits(target) {
const targetRow = document.querySelector("#splits-" + target);
const summaryRow = document.querySelector("#splits-total");
if (startTime) {
const time = performance.now() - startTime;
const segmentTime = performance.now() - (lastSplitTime || startTime);
summaryRow.querySelector(".splits-time").innerHTML = formatTime(time);
targetRow.querySelector(".splits-time").innerHTML = formatTime(time);
if (bestSplits["bestRun"] && target in bestSplits["bestRun"]) {
let bestRunSegment = bestSplits["bestRun"][target];
let previousTime = bestRunTime(target);
const deltaNode = targetRow.querySelector(".splits-delta");
deltaNode.classList.remove("splits-ahead");
deltaNode.classList.remove("splits-behind");
deltaNode.classList.remove("splits-gain");
deltaNode.classList.remove("splits-loss");
deltaNode.classList.remove("splits-glod");
if (time <= previousTime)
deltaNode.classList.add("splits-ahead");
if (time > previousTime)
deltaNode.classList.add("splits-behind");
if (segmentTime > bestRunSegment)
deltaNode.classList.add("splits-loss");
if (segmentTime <= bestRunSegment)
deltaNode.classList.add("splits-gain");
const timeStr = formatTime(time - previousTime);
deltaNode.innerHTML = (time > previousTime ? "+" : "") + timeStr;
}
}
else {
summaryRow.querySelector(".splits-time").innerHTML = formatTime(0);
targetRow.querySelector(".splits-time").innerHTML = formatTime(0);
}
targetRow.querySelector(".splits-num-moves").innerHTML = numMovesMadeSinceSplit;
summaryRow.querySelector(".splits-num-moves").innerHTML = totalMovesMade;
}
function storeSplit(target, time) {
splits[target] = time;
if (!bestSplits[target] || time <= bestSplits[target]) {
bestSplits[target] = time;
if (target == "total") {
bestSplits["bestRun"] = splits;
document.querySelector("#splits-total .splits-time").classList.add("splits-pb");
}
else {
document.querySelector("#splits-" + target + " .splits-time").classList.add("splits-glod");
}
}
localStorage.setItem(startPosition, JSON.stringify(bestSplits));
let sumOfBest = 0;
targets.forEach(square => {
if (square in bestSplits && sumOfBest != null) sumOfBest += bestSplits[square];
else sumOfBest = null;
});
if (sumOfBest != null) {
document.querySelector("#splits-best .splits-best-time").innerHTML =
formatTime(sumOfBest);
}
}
function updateTarget() {
while (targetId < targets.length && game.get(targets[targetId])) {
updateSplits(targets[targetId]);
numMovesMadeSinceSplit = 0;
const endTime = performance.now();
const splitStart = lastSplitTime || startTime || endTime;
storeSplit(targets[targetId], endTime - splitStart);
lastSplitTime = endTime;
targetId += 1;
if (targetId == targets.length)
storeSplit("total", endTime - startTime);
}
let shapes = [];
if (showInaccessible)
shapes = coveredSquaresHighlights();
if (targetId < targets.length) {
shapes.push({orig: targets[targetId], brush: "green"});
}
board.set({drawable: {shapes: shapes}});
}
function coveredSquares() {
let reachable = reachableSquares();
let squares = [];
for (let x = 0; x < 8; x++) {
for (let y = 0; y < 8; y++) {
let file = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][8 - y - 1];
let rank = ((8 - x - 1) + 1).toString();
let square = file + rank;
if (!reachable[square])
squares.push(square);
}
}
return squares;
}
function coveredSquaresHighlights() {
return coveredSquares().map(s => { return {orig: s, brush: "red"} });
}
function onMove(source, target, meta) {
totalMovesMade++;
numMovesMadeSinceSplit++;
game.put(game.get(source), target);
game.remove(source);
updateLegalMoves();
if (targetId < targets.length) {
updateSplits(targets[targetId]);
if (!startTime) updateTimer(performance.now());
}
updateTarget();
board.selectSquare(target, true);
}
function resizeBoard() {
const viewportWidth =
Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const sideBySideWidth = viewportWidth -
(document.querySelector("#splits thead").clientWidth || 0);
const viewportHeight = Math.max(document.documentElement.clientHeight|| 0,
window.innerHeight || 0) - 100;
const usedWidth = sideBySideWidth > 320 ? sideBySideWidth : viewportWidth;
const size = Math.min(usedWidth, viewportHeight).toString() + "px";
document.querySelector("#board").style.width = size;
document.querySelector("#board").style.height = size;
if (sideBySideWidth > 300) {
document.querySelector("#board-container").classList.add("board-container-side-by-side");
document.querySelector("#board-column").style.flex = size;
}
else {
document.querySelector("#board-container").classList.remove("board-container-side-by-side");
document.querySelector("#board-column").style.flex = null;
}
}
function soundOnMove(source, target, capture) {
new Audio('chess-move.ogg').play();
}
function onSelect(square) {
if (!square || !game.get(square)) {
square = currentSquare(game);
board.selectSquare(square);
}
}
let board = null;
let startPosition = "7n/8/8/3Q4/8/8/8/8 b - - 0 1";
let game = Chess(startPosition);
let startTime = null;
let showInaccessible = false;
let timerUpdateRequest = null;
let totalMovesMade = 0;
let numMovesMadeSinceSplit = 0;
let lastSplitTime = null;
let targets = targetSquares();
let targetId = 0;
let splits = {};
let bestSplits = {};
if (localStorage.getItem(startPosition))
bestSplits = JSON.parse(localStorage.getItem(startPosition));
function setupPosition(fen) {
if (timerUpdateRequest)
cancelAnimationFrame(timerUpdateRequest);
clearSplits();
startPosition = fen;
game = Chess(startPosition);
startTime = null;
totalMovesMade = 0;
numMovesMadeSinceSplit = 0;
lastSplitTime = null;
targets = targetSquares()
targetId = 0;
splits = {};
bestSplits = {};
if (localStorage.getItem(startPosition))
bestSplits = JSON.parse(localStorage.getItem(startPosition));
board.set(defaultConfig());
createSplits();
updateLegalMoves(board);
updateTarget();
resizeBoard();
}
function defaultConfig() {
const isWhite = game.turn() == game.WHITE;
return {
fen: game.fen(),
orientation: "white",
movable: {
free: false,
color: isWhite ? "white" : "black",
showDests: false,
events: {after: onMove},
},
selected: currentSquare(game),
drawable: {enabled: false},
animation: {enabled: false},
events: {move: soundOnMove, select: onSelect},
coordinates: false
};
}
document.addEventListener("DOMContentLoaded", function() {
board = Chessground.Chessground(document.querySelector("#board"), defaultConfig());
createSplits();
updateLegalMoves(board);
updateTarget();
resizeBoard();
window.addEventListener("resize", resizeBoard);
const showInaccessibleBox = document.querySelector("#show-inaccessible");
showInaccessibleBox.onchange = () => {
showInaccessible = showInaccessibleBox.checked;
updateTarget();
};
const fenSelector = document.querySelector("#fen-selector");
const fenInput = document.querySelector("#fen");
fenSelector.onchange = () => {
fenInput.value = fenSelector.value;
};
document.querySelector("#load-fen").onclick = () => {
setupPosition(fenInput.value);
};
});
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment