waves/public/assets/g/chess/other-implementations/example2/js/board.js
2025-04-09 17:11:14 -05:00

1942 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* exported BOARD */
var BOARD = function board_init(el, options)
{
"use strict";
var board,
board_details = {
ranks: 8,
files: 8,
},
squares,
hover_squares,
pos,
colors = ["blue", "red", "green", "yellow", "teal", "orange", "purple", "pink"],
///NOTE: These should match the CSS.
rgba = ["rgba(0, 0, 240, .6)", "rgba(240, 0, 0, .6)", "rgba(0, 240, 0, .6)", "rgba(240, 240, 0, .6)", "rgba(0, 240, 240, .6)", "rgba(240, 120, 0, .6)", "rgba(120, 0, 120, .6)", "rgba(240, 0, 240, .6)"],
rook_arrow_color = "rgba(0, 0, 240, .2)",
cur_color = 0,
capturing_clicks,
legal_moves,
arrow_manager,
dragging_arrow = {},
mode = "setup",
last_fen,
fastDrag,
setInitialDraggingPosition,
isFlipped = false;
function num_to_alpha(num)
{
return "abcdefgh"[num];
}
function error(str)
{
str = str || "Unknown error";
alert("An error occured.\n" + str);
throw new Error(str);
}
function check_el(el)
{
if (typeof el === "string") {
return document.getElementById(el);
}
return el;
}
function flip(force)
{
if ((isFlipped && force !== true) || force === false) {
board.el.classList.remove("flipped");
isFlipped = false;
} else {
board.el.classList.add("flipped");
isFlipped = true;
}
size_board(board_details.width, board_details.height);
}
function get_init_pos()
{
///NOTE: I made this a function so that we could pass other arguments, like chess varients.
return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
//return "6R1/1pp5/5k2/p1b4r/P1P2p2/1P5r/4R2P/7K w - - 0 39";
}
function remove_square_focus(x, y)
{
if (squares[y][x].focus_color) {
squares[y][x].classList.remove("focus_square_" + squares[y][x].focus_color);
squares[y][x].classList.remove("focusSquare");
delete squares[y][x].focus_color;
}
}
function focus_square(x, y, color)
{
remove_square_focus(x, y);
if (color && colors.indexOf(color) > -1) {
squares[y][x].focus_color = color;
squares[y][x].classList.add("focus_square_" + color);
squares[y][x].classList.add("focusSquare");
}
}
function clear_focuses()
{
delete board.clicked_piece;
squares.forEach(function oneach(file, y)
{
file.forEach(function oneach(sq, x)
{
remove_square_focus(x, y);
});
});
}
function remove_highlight(y, x)
{
if (hover_squares[y][x].highlight_color) {
hover_squares[y][x].classList.remove(hover_squares[y][x].highlight_color);
delete hover_squares[y][x].highlight_color;
}
}
function highlight_square(y, x, color)
{
remove_highlight(y, x);
if (color && colors.indexOf(color) > -1) {
hover_squares[y][x].highlight_color = color;
hover_squares[y][x].classList.add(color);
}
}
function clear_highlights()
{
hover_squares.forEach(function oneach(file, y)
{
file.forEach(function oneach(sq, x)
{
remove_highlight(y, x);
});
});
}
/**
* Ctrl click to set/remove colors.
* Ctrl Left/Right to change colors.
* Ctrl Non-left click to (only/always) remove colors.
* Ctrl Space to clear board of highlights.
*/
function hover_square_click_maker(x, y)
{
return function (e)
{
var new_color,
square;
if (e.ctrlKey) {
if (!dragging_arrow.drew_arrow) {
/// Highlight the sqaure.
new_color = colors[cur_color];
if (is_left_click(e)) {
if (hover_squares[y][x].highlight_color === new_color) {
remove_highlight(y, x);
} else {
highlight_square(y, x, new_color);
}
} else {
remove_highlight(y, x);
e.preventDefault();
}
}
} else if (board.clicked_piece) {
///TODO: Make sure the move is valid.
/// Move to the square.
square = {rank: y, file: x};
make_move(board.clicked_piece.piece, square, get_move(board.clicked_piece.piece, square), is_promoting(board.clicked_piece.piece, square));
}
};
}
function arrow_start_maker(rank, file)
{
return function (e)
{
if (e.ctrlKey) {
dragging_arrow.drew_arrow = false;
dragging_arrow.start_square = {rank: rank, file: file};
}
};
}
function arrow_move_maker(rank, file)
{
function finish_arrow()
{
delete dragging_arrow.start_square;
delete dragging_arrow.cur_square;
delete dragging_arrow.number;
}
return function (e)
{
if (dragging_arrow.start_square) {
if (G.normalize_mouse_buttons(e) === 1) {
if (!dragging_arrow.cur_square || rank !== dragging_arrow.cur_square.rank || file !== dragging_arrow.cur_square.file) {
if (typeof dragging_arrow.number === "number") {
arrow_manager.delete_arrow(dragging_arrow.number);
delete dragging_arrow.cur_square;
}
if (dragging_arrow.start_square.rank !== rank || dragging_arrow.start_square.file !== file) {
dragging_arrow.number = arrow_manager.draw(dragging_arrow.start_square.rank, dragging_arrow.start_square.file, rank, file, rgba[cur_color])
dragging_arrow.cur_square = {rank: rank, file: file};
dragging_arrow.drew_arrow = true;
}
}
if (e.type === "mouseup") {
finish_arrow();
}
} else {
finish_arrow();
}
}
};
}
function make_hover_square(x, y)
{
var el = document.createElement("div");
el.classList.add("hoverSquare");
el.classList.add("rank" + y);
el.classList.add("file" + x);
el.addEventListener("click", hover_square_click_maker(x, y));
el.addEventListener("mousedown", arrow_start_maker(y, x));
el.addEventListener("mousemove", arrow_move_maker(y, x));
el.addEventListener("mouseup", arrow_move_maker(y, x));
return el;
}
function get_rank_file_from_str(str)
{
return {rank: str[1] - 1, file: str.charCodeAt(0) - 97};
}
function remove_dot(x, y)
{
if (hover_squares[y][x].dot_color) {
hover_squares[y][x].classList.remove("dot_square_" + hover_squares[y][x].dot_color);
hover_squares[y][x].classList.remove("dotSquare");
delete hover_squares[y][x].dot_color;
}
}
function clear_dots()
{
hover_squares.forEach(function oneach(file, y)
{
file.forEach(function oneach(sq, x)
{
remove_dot(x, y);
});
});
}
function add_dot(x, y, color)
{
remove_dot(x, y);
if (color && colors.indexOf(color) > -1) {
hover_squares[y][x].dot_color = color;
hover_squares[y][x].classList.add("dot_square_" + color);
hover_squares[y][x].classList.add("dotSquare");
}
}
function add_clickabe_square(move_data)
{
if (board.clicked_piece) {
if (!board.clicked_piece.clickable_squares) {
board.clicked_piece.clickable_squares = [];
}
board.clicked_piece.clickable_squares.push(move_data);
}
}
function get_piece_start_square(piece)
{
return get_file_letter(piece.file) + (piece.rank + 1);
}
function show_legal_moves(piece)
{
var start_sq = get_piece_start_square(piece);
if (legal_moves && legal_moves.uci) {
legal_moves.uci.forEach(function oneach(move, i)
{
var move_data,
color;
if (move.indexOf(start_sq) === 0) {
move_data = get_rank_file_from_str(move.substr(2));
///NOTE: We can't use get_piece_from_rank_file(move_data.rank, move_data.file) because it won't find en passant.
if (legal_moves.san[i].indexOf("x") === -1) {
color = "green";
} else {
color = "red";
}
add_dot(move_data.file, move_data.rank, color);
add_clickabe_square(move_data);
}
});
}
}
function make_square(x, y)
{
var el = document.createElement("div");
el.classList.add("square");
el.classList.add("rank" + y);
el.classList.add("file" + x);
if ((x + y) % 2) {
el.classList.add("light");
} else {
el.classList.add("dark");
}
return el;
}
function make_rank(num)
{
var el = document.createElement("div");
el.classList.add("rank");
el.classList.add("rank" + num);
return el;
}
function size_board(w, h)
{
var h_snap = h % board.board_details.ranks,
w_snap = w % board.board_details.files;
w -= w_snap;
h -= h_snap;
board_details.width = parseFloat(w);
board_details.height = parseFloat(h);
board.el.style.width = board_details.width + "px";
board.el.style.height = board_details.height + "px";
G.events.trigger("board_resize", {w: w, h: h});
}
function make_board_num(num)
{
var el = document.createElement("div");
el.classList.add("notation");
el.classList.add("num");
el.textContent = num + 1;
return el;
}
function get_file_letter(num)
{
return String.fromCharCode(97 + num);
}
function make_board_letter(num)
{
var el = document.createElement("div");
el.classList.add("notation");
el.classList.add("letter");
el.textContent = get_file_letter(num);
return el;
}
function switch_turn()
{
var last_turn = board.turn;
if (board.turn === "w") {
board.turn = "b";
} else {
board.turn = "w";
}
G.events.trigger("board_turn_switch", {turn: board.turn, last_turn: last_turn});
}
function create_board(el, dim)
{
var x,
y,
cur_rank;
if (el) {
board.el = check_el(el);
}
board.el.innerHTML = "";
/// Prevent I beam cursor.
board.el.addEventListener("mousedown", function onboard_mouse_down(e)
{
e.preventDefault();
});
if (dim) {
size_board(dim.w, dim.h);
} else {
size_board(600, 600);
}
squares = [];
hover_squares = [];
for (y = board_details.ranks - 1; y >= 0; y -= 1) {
squares[y] = [];
hover_squares[y] = [];
for (x = 0; x < board_details.files; x += 1) {
squares[y][x] = make_square(x, y);
hover_squares[y][x] = make_hover_square(x, y);
if (x === 0) {
cur_rank = make_rank(y);
board.el.appendChild(cur_rank);
squares[y][x].appendChild(make_board_num(y));
}
if (y === 0) {
squares[y][x].appendChild(make_board_letter(x));
}
squares[y][x].appendChild(hover_squares[y][x]);
cur_rank.appendChild(squares[y][x]);
}
}
board.el.classList.add("chess_board");
return board;
}
function load_pieces_from_start(fen)
{
var fen_pieces = fen.match(/^\S+/),
rank = 7,
file = 0,
id = 0,
piece_count = 0,
create_pieces;
delete board.last_move;
if (fen !== last_fen) {
create_pieces = true;
if (board.pieces) {
board.pieces.forEach(function oneach(piece)
{
if (piece.el && piece.el.parentNode) {
piece.el.parentNode.removeChild(piece.el);
}
});
}
board.pieces = [];
}
last_fen = fen;
if (!fen_pieces) {
error("Bad position: " + pos);
}
fen_pieces[0].split("").forEach(function oneach(letter)
{
var piece;
if (letter === "/") {
rank -= 1;
file = 0;
} else if (/\d/.test(letter)) {
file += parseInt(letter, 10);
} else { /// It's a piece.
if (create_pieces) {
piece = {};
/// Is it white?
if (/[A-Z]/.test(letter)) {
piece.color = "w";
} else {
piece.color = "b";
}
piece.id = id;
board.pieces[piece_count] = piece;
}
/// We do, however, always need to set the starting rank and file.
board.pieces[piece_count].rank = rank;
board.pieces[piece_count].file = file;
/// We also need to set the type, in case it was a pawn that promoted.
board.pieces[piece_count].type = letter.toLowerCase();
file += 1;
id += 1;
piece_count += 1;
}
});
}
function is_piece_moveable(piece)
{
return board.get_mode() === "setup" || (board.get_mode() === "play" && board.turn === piece.color && board.players[board.turn].type === "human");
}
function is_left_click(e)
{
return (e.which || (e || window.event).button) === 1;
}
function fix_touch_event(e)
{
if (e.changedTouches && e.changedTouches[0]) {
e.clientX = e.changedTouches[0].pageX;
e.clientY = e.changedTouches[0].pageY;
}
}
function select_piece(rank, file)
{
focus_piece_for_moving(get_piece_from_rank_file(rank, file));
}
function focus_piece_for_moving(piece)
{
board.clicked_piece = {piece: piece};
focus_square(piece.file, piece.rank, "green");
show_legal_moves(piece);
G.events.trigger("focus_piece", {piece: {rank: piece.rank, file: piece.file, color: piece.color, type: piece.type}});
}
function add_piece_events(piece)
{
function onpiece_mouse_down(e)
{
///TODO: Test and make sure it works on touch devices.
if ((e.type === "touchstart" || is_left_click(e)) && is_piece_moveable(piece)) {
fix_touch_event(e);
board.dragging = {};
board.dragging.piece = piece;
board.dragging.box = piece.el.getBoundingClientRect();
board.dragging.origin = {x: e.clientX, y: e.clientY};
board.dragging.offset = {
x: board.dragging.origin.x - (board.dragging.box.left + (board.dragging.box.width /2)),
y: board.dragging.origin.y - (board.dragging.box.top + (board.dragging.box.height /2))
};
board.el.classList.add("dragging");
board.dragging.piece.el.classList.add("dragging");
fastDrag = 0;
setInitialDraggingPosition = setTimeout(function ()
{
onmousemove(e)
}, 300);
}
if (e.preventDefault) {
/// Prevent the cursor from becoming an I beam.
e.preventDefault();
}
if (board.get_mode() === "play") {
if (board.clicked_piece && board.clicked_piece.piece) {
remove_square_focus(board.clicked_piece.piece.file, board.clicked_piece.piece.rank);
clear_dots();
/// If the king was previously selected, we want to refocus it.
if (board.checked_king) {
focus_square(board.checked_king.file, board.checked_king.rank, "red");
}
}
if (is_piece_moveable(piece)) {
focus_piece_for_moving(piece);
}
}
}
piece.el.addEventListener("mousedown", onpiece_mouse_down);
piece.el.addEventListener("touchstart", onpiece_mouse_down);
}
function css_transform(el, prop, value)
{
if (isFlipped) {
value += " rotateZ(180deg)";
}
el.style[prop] = value;
el.style["Webkit" + prop[0].toUpperCase() + prop.substr(1)] = value;
el.style["O" + prop[0].toUpperCase() + prop.substr(1)] = value;
el.style["MS" + prop[0].toUpperCase() + prop.substr(1)] = value;
el.style["Moz" + prop[0].toUpperCase() + prop.substr(1)] = value;
}
function onmousemove(e)
{
var x, y;
/// If the user held the ctrl button and then clicked off of the browser, it will still be marked as capturing. We remove that here.
if (capturing_clicks && !e.ctrlKey) {
stop_capturing_clicks();
}
if (board.dragging && board.dragging.piece) {
fix_touch_event(e);
x = e.clientX - board.dragging.origin.x + board.dragging.offset.x;
y = e.clientY - board.dragging.origin.y + board.dragging.offset.y;
if (isFlipped) {
x *= -1;
y *= -1;
}
css_transform(board.dragging.piece.el, "transform", "translate(" + x + "px," + y + "px)");
if (!fastDrag) {
clearInterval(setInitialDraggingPosition);
fastDrag = setTimeout(function ()
{
if (board.dragging && board.dragging.piece && board.dragging.piece.el) {
board.dragging.piece.el.classList.add("fastDrag");
}
}, 75);
}
}
}
function get_dragging_hovering_square(e)
{
fix_touch_event(e);
var el,
match,
square = {},
rank_m,
file_m,
x = e.clientX,
y = e.clientY;
el = document.elementFromPoint(x, y);
if (el && (el.className && el.classList && el.classList.contains("square") || el.classList.contains("hoverSquare"))) {
rank_m = el.className.match(/rank(\d+)/);
file_m = el.className.match(/file(\d+)/);
if (rank_m) {
square.rank = parseInt(rank_m[1], 10);
}
if (file_m) {
square.file = parseInt(file_m[1], 10);
}
}
if (!isNaN(square.rank) && !isNaN(square.file)) {
square.el = el;
return square;
}
}
function is_legal_move(uci)
{
if (!legal_moves || !legal_moves.uci) {
return false;
}
return legal_moves.uci.indexOf(uci) > -1;
}
function get_move(starting, ending)
{
var str;
if (starting && ending) {
str = get_file_letter(starting.file) + (parseInt(starting.rank, 10) + 1) + get_file_letter(ending.file) + (parseInt(ending.rank, 10) + 1);
if (is_promoting(starting, ending)) {
str += "q"; /// We just add something to make sure it's a legal move. We'll ask the user later what he actually wants to promote to.
}
}
return str;
}
function create_promotion_icon(which, piece, cb)
{
var icon = document.createElement("div");
icon.addEventListener("click", function onclick()
{
cb(which);
});
/// In play mode, we can go with the color; in setup mode, we need to get the color from the piece.
icon.style.backgroundImage = get_piece_img({color: board.get_mode() === "play" ? board.turn : piece.color, type: which});
icon.classList.add("promotion_icon");
return icon;
}
function create_modular_window(options)
{
var mod_win = G.cde("div", {c: "board_modular_window"}),
old_mode,
modular_mode = "waiting_for_modular_window";
function close_window()
{
delete board.close_modular_window;
document.body.removeChild(mod_win);
if (!options.dont_change_mode && board.get_mode() === modular_mode) {
board.set_mode(old_mode);
}
window.removeEventListener("keydown", listen_for_close);
}
function open_window()
{
if (board.close_modular_window) {
return setTimeout(open_window, 200);
}
board.close_modular_window = close_window;
document.body.appendChild(mod_win);
if (!options.dont_change_mode) {
old_mode = board.get_mode();
board.set_mode(modular_mode);
}
}
function listen_for_close(e)
{
if (e.keyCode === 27) { /// escape
close_window();
}
}
function add_x()
{
mod_win.appendChild(G.cde("div", {t: "X", c: "xButton"}, {click: close_window}));
}
if (options) {
if (options.content) {
if (typeof options.content === "object") {
mod_win.appendChild(options.content);
} else {
mod_win.innerHTML = options.content;
}
}
if (options.cancelable) {
window.addEventListener("keydown", listen_for_close);
add_x();
}
if (options.open) {
open_window();
}
} else {
options = {};
}
return {
close: close_window,
open: open_window,
el: mod_win,
}
}
function promotion_prompt(piece, cb)
{
var modular_window = create_modular_window();
function onselect(which)
{
modular_window.close();
cb(which);
}
modular_window.el.appendChild(G.cde("div", {t:"Promote to", c: "promotion_text"}));
modular_window.el.appendChild(create_promotion_icon("q", piece, onselect));
modular_window.el.appendChild(create_promotion_icon("r", piece, onselect));
modular_window.el.appendChild(create_promotion_icon("b", piece, onselect));
modular_window.el.appendChild(create_promotion_icon("n", piece, onselect));
modular_window.open();
}
function report_move(uci, promoting, piece, cb)
{
/// We make it async because of promotion.
function record()
{
var san = get_san(uci);
legal_moves = null;
if (board.get_mode() === "play") {
track_move(uci, san);
if (board.onmove) {
board.onmove(uci, san);
}
}
if (cb) {
cb(uci);
}
}
if (promoting) {
promotion_prompt(piece, function onres(answer)
{
///NOTE: The uci move already includes a promotion to queen to make it a valid move. We need to remove this and replace it with the desired promotion.
uci = uci.substr(0, 4) + answer;
record();
});
} else {
setTimeout(record, 10);
}
}
function set_piece_pos(piece, square, do_not_save)
{
if (!piece || !piece.el || !piece.el.style || !square) {
return;
}
piece.el.style.top = -(square.rank * 100) + "%";
piece.el.style.bottom = (square.rank * 100) + "%";
piece.el.style.left = (square.file * 100) + "%";
piece.el.style.right = -(square.file * 100) + "%";
if (!do_not_save) {
piece.rank = square.rank;
piece.file = square.file;
}
}
function get_san(uci)
{
if (!legal_moves || !legal_moves.uci || !legal_moves.san) {
return;
}
return legal_moves.san[legal_moves.uci.indexOf(uci)];
}
function set_image(piece)
{
var img = get_piece_img(piece);
/// Don't set it if it's the same.
if (piece.backgroundImage !== img) {
piece.backgroundImage = img;
piece.el.style.backgroundImage = img;
}
}
function promote_piece(piece, uci)
{
if (piece && uci.length === 5 && /[qrbn]/.test(uci[4])) {
piece.type = uci[4];
set_image(piece);
}
}
function mark_ep(uci)
{
var index
if (!legal_moves || !legal_moves.uci || !legal_moves.san) {
return;
}
index = legal_moves.uci.indexOf(uci);
if (legal_moves.san && legal_moves.san[index] && legal_moves.san[index].indexOf("e.p.") === -1 && legal_moves.san[index].indexOf("(ep)") === -1) {
/// Add the notation after the move notation but before check(mate) symbol.
///NOTE: A pawn could check(mate) and en passant at the same time, but not promote.
legal_moves.san[index] = legal_moves.san[index].substr(0, 4) + "e.p." + legal_moves.san[index].substr(4);
}
}
function move_piece(piece, square, uci)
{
var captured_piece,
rook,
san = get_san(uci),
rook_rank = board.turn === "w" ? 0 : 7; ///TODO: Use board_details.ranks
if (!piece || !square || !uci) {
return false;
}
///NOTE: This does not find en passant captures. See below.
captured_piece = get_piece_from_rank_file(square.rank, square.file);
if (board.get_mode() === "play") {
/// Indicate that the board has been changed; it is not in the inital starting position.
board.messy = true;
/// En passant
if (!captured_piece && piece.type === "p" && piece.file !== square.file && ((piece.color === "w" && square.rank === board_details.ranks - 3) || (piece.color === "b" && square.rank === 2))) {
captured_piece = get_piece_from_rank_file(piece.rank, square.file);
mark_ep(uci);
}
if (captured_piece && captured_piece.id !== piece.id) {
capture(captured_piece);
}
/// Is it castling?
if (san === "O-O" || san === "0-0") { /// Kingside castle
rook = get_piece_from_rank_file(rook_rank, board_details.files - 1);
set_piece_pos(rook, {rank: rook_rank, file: board_details.files - 3});
} else if (san === "O-O-O" || san === "0-0-0") { /// Queenside castle
rook = get_piece_from_rank_file(rook_rank, 0);
set_piece_pos(rook, {rank: rook_rank, file: 3});
}
} else if (board.get_mode() === "setup" && captured_piece) {
/// The pieces should swap places.
set_piece_pos(captured_piece, piece);
if (captured_piece.type === "p" && (captured_piece.rank === 0 || captured_piece.rank === board_details.ranks - 1)) {
promotion_prompt(captured_piece, function onres(answer)
{
promote_piece(captured_piece, num_to_alpha(square.file) + square.rank + num_to_alpha(piece.file) + piece.rank + answer);
});
}
}
/// Make sure to change the rank and file after checking for a capured piece so that you don't capture yourself.
set_piece_pos(piece, square);
}
function is_promoting(piece, square)
{
if (!piece || !square) {
return;
}
return piece.type === "p" && square.rank % (board_details.ranks - 1) === 0;
}
function remove_piece(piece)
{
var i;
function remove()
{
piece.el.parentNode.removeChild(piece.el);
}
for (i = board.pieces.length - 1; i >= 0; i -= 1) {
if (piece.id === board.pieces[i].id) {
G.array_remove(board.pieces, i);
/// Make it fade out.
piece.el.classList.add("captured");
setTimeout(remove, 2000);
return;
}
}
}
function make_move(piece, square, uci, promoting)
{
var oldRank = piece.rank;
var oldFile = piece.file;
move_piece(piece, square, uci);
report_move(uci, promoting, piece, function onreport(finalized_uci)
{
///NOTE: Since this is async, we need to store which piece was moved.
promote_piece(piece, finalized_uci);
G.events.trigger("board_human_move", {
color: piece.color,
oldRank: oldRank,
oldFile: oldFile,
rank: piece.rank,
file: piece.file,
type: piece.type,
promoted: promoting,
from: get_piece_start_square({rank: oldRank, file: oldFile}),
to: get_piece_start_square(piece),
uci: finalized_uci
});
});
}
function onmouseup(e)
{
var square,
uci,
promoting,
piece;
if (board.dragging && board.dragging.piece) {
square = get_dragging_hovering_square(e);
promoting = is_promoting(board.dragging.piece, square);
piece = board.dragging.piece;
uci = get_move(piece, square);
if (square && (board.get_mode() === "setup" || is_legal_move(uci))) {
make_move(piece, square, uci, promoting);
} else {
/// Snap back.
if (board.get_mode() === "setup" && !board.noRemoving) {
remove_piece(piece);
/// We need to remove "dragging" to make the transitions work again.
piece.el.classList.remove("dragging");
clearTimeout(fastDrag);
piece.el.classList.remove("fastDrag");
delete board.dragging.piece;
}
}
/// If it wasn't deleted
if (board.dragging.piece) {
/// Make the piece immediately move (no bouncing)
piece.el.classList.add("snap");
setTimeout(function ()
{
/// Re-enable smooth moving.
piece.el.classList.remove("snap");
}, 10);
piece.el.style.transform = "";
piece.el.classList.remove("dragging");
/// Fast dragging is to set the initial position. It's not needed now.
clearTimeout(fastDrag);
piece.el.classList.remove("fastDrag");
}
board.el.classList.remove("dragging");
delete board.dragging;
}
}
function get_piece_img(piece)
{
return "url(\"" + encodeURI(board.pieces_path + board.theme + (board.theme ? "/" : "") + piece.color + piece.type + (board.theme_ext || ".svg")) + "\")";
}
function clear_board_extras()
{
clear_highlights();
clear_focuses();
clear_dots();
arrow_manager.clear();
}
function add_piece(info)
{
var piece = {
color: info.color,
rank: info.rank,
file: info.file,
type: info.type,
};
var last_piece = board.pieces[board.pieces.length - 1];
if (last_piece) {
piece.id = last_piece.id + 1;
} else {
piece.id = 0;
}
board.pieces.push(piece);
insert_piece(piece);
/// If the pieces were already on the board from a previous game, a pawn may have promoted.
set_image(piece);
set_piece_pos(piece, {rank: piece.rank, file: piece.file});
last_fen = get_fen();
}
function insert_piece(piece)
{
piece.el = document.createElement("div");
piece.el.classList.add("piece");
add_piece_events(piece);
/// We just put them all in the bottom left corner and move the position.
squares[0][0].appendChild(piece.el);
}
function set_board(fen)
{
var matches;
delete board.last_move;
fen = fen || get_init_pos();
load_pieces_from_start(fen);
board.pieces.forEach(function oneach(piece)
{
if (!piece.el) {
insert_piece(piece);
}
/// If the pieces were already on the board from a previous game, a pawn may have promoted.
set_image(piece);
/// If the pieces were already on the board from a previous game, they may have been captured.
if (piece.captured) {
release(piece);
}
set_piece_pos(piece, {rank: piece.rank, file: piece.file});
});
clear_board_extras();
matches = fen.match(/^\S+ ([wb])/);
if (matches) {
board.turn = matches[1];
} else {
board.turn = "w";
}
board.moves = [];
board.messy = false;
if (typeof board.close_modular_window === "function") {
board.close_modular_window();
}
}
function wait()
{
board.set_mode("wait")
board.el.classList.add("waiting");
board.el.classList.remove("settingUp");
board.el.classList.remove("playing");
arrow_manager.el.classList.add("waiting");
}
function play()
{
board.set_mode("play")
board.el.classList.remove("waiting");
board.el.classList.remove("settingUp");
board.el.classList.add("playing");
arrow_manager.el.classList.remove("waiting");
delete board.last_move;
}
function enable_setup()
{
board.set_mode("setup")
board.el.classList.remove("waiting");
board.el.classList.remove("playing");
board.el.classList.add("settingUp");
arrow_manager.el.classList.remove("waiting");
delete board.last_move;
}
function get_piece_from_rank_file(rank, file)
{
var i;
rank = parseInt(rank, 10);
file = parseInt(file, 10);
for (i = board.pieces.length - 1; i >= 0; i -= 1) {
if (!board.pieces[i].captured && board.pieces[i].rank === rank && board.pieces[i].file === file) {
return board.pieces[i];
}
}
}
function split_uci(uci)
{
var positions = {
starting: {
file: uci.charCodeAt(0) - 97,
rank: parseInt(uci[1], 10) - 1
},
ending: {
file: uci.charCodeAt(2) - 97,
rank: parseInt(uci[3], 10) - 1
}
};
if (uci.length === 5) {
positions.promote_to = uci[4];
}
return positions;
}
function capture(piece)
{
piece.captured = true;
piece.el.classList.add("captured");
}
function release(piece)
{
delete piece.captured;
piece.el.classList.remove("captured");
}
function move_piece_uci(uci)
{
var positions = split_uci(uci),
piece,
ending_square;
ending_square = {
el: squares[positions.ending.rank][positions.ending.file],
rank: positions.ending.rank,
file: positions.ending.file
};
piece = get_piece_from_rank_file(positions.starting.rank, positions.starting.file);
if (piece) {
move_piece(piece, ending_square, uci);
promote_piece(piece, uci);
}
}
function track_move(uci, san)
{
board.moves.push(uci);
switch_turn();
clear_board_extras();
G.events.trigger("board_move", {uci: uci, san: san});
board.last_move = {uci: uci, san: san};
}
function move_backward(data)
{
var cur_move_data,
moving_peice,
rook_data,
rook_peice;
/// First, set the fen to the previous (the move we're going to).
/// Then, move the peice(s) to where they were in the current move (or what was the current move).
/// Next move them back after a delay (so the CSS transition takes effect).
/// Finally draw the arrow for the previous move (if any).
/// Step 1
board.set_board(data.prev_fen);
/// Step 2
cur_move_data = split_uci(data.cur_uci);
if (cur_move_data) {
moving_peice = get_piece_from_rank_file(cur_move_data.starting.rank, cur_move_data.starting.file);
if (moving_peice) {
if (data.cur_san === "O-O" || data.cur_san === "0-0") {
rook_data = {
prev_file: board_details.files - 1,
cur_file: cur_move_data.ending.file - 1
};
} else if (data.cur_san === "O-O-O" || data.cur_san === "0-0-0") {
rook_data = {
prev_file: 0,
cur_file: cur_move_data.ending.file + 1
};
}
if (rook_data) {
rook_peice = get_piece_from_rank_file(cur_move_data.starting.rank, rook_data.prev_file);
}
set_piece_pos(moving_peice, cur_move_data.ending, true);
if (rook_peice) {
set_piece_pos(rook_peice, {rank: cur_move_data.starting.rank, file: rook_data.cur_file}, true);
}
/// Step 3
setTimeout(function ()
{
/// Make sure it hasn't move in the mean time.
if (moving_peice.rank === cur_move_data.starting.rank && moving_peice.file === cur_move_data.starting.file) {
///HACK: Try to force a reflow.
window.getComputedStyle(moving_peice.el).top;
set_piece_pos(moving_peice, cur_move_data.starting, true);
if (rook_peice && rook_peice.rank === cur_move_data.starting.rank && rook_peice.file === rook_data.prev_file) {
set_piece_pos(rook_peice, {rank: cur_move_data.starting.rank, file: rook_data.prev_file}, true);
}
}
}, 50);
}
}
/// Step 4
if (data.prev_uci) {
arrow_manager.arrow_onmove({uci: data.prev_uci, san: data.prev_san});
}
}
function move(data)
{
var san,
uci;
/// If it's a string, it's just a uci move. If it's an object, it is data for moving backward.
if (typeof data === "string") {
uci = data.toLowerCase();
san = get_san(uci);
move_piece_uci(uci);
if (board.get_mode() !== "setup") {
track_move(uci, san);
}
} else {
move_backward(data);
}
}
function onkeydown(e)
{
var target = e.target || e.srcElement || e.originalTarget;
if (e.ctrlKey) {
board.el.classList.add("catchClicks");
capturing_clicks = true;
if (e.keyCode === 39) { /// Right
cur_color += 1;
if (cur_color >= colors.length) {
cur_color = 0;
}
} else if (e.keyCode === 37) { /// Left
cur_color -= 1;
if (cur_color < 0) {
cur_color = colors.length - 1;
}
} else if (e.keyCode === 32) { /// Space
clear_highlights();
arrow_manager.clear(true); /// Only clear lines drawn by the user.
}
}
if (e.keyCode === 8 && (!target || target.tagName === "BODY")) { /// backspace
arrow_manager.delete_arrow();
e.preventDefault();
}
}
function stop_capturing_clicks()
{
board.el.classList.remove("catchClicks");
capturing_clicks = false;
}
function onkeyup(e)
{
if (!e.ctrlKey) {
stop_capturing_clicks();
}
}
function get_fen(full)
{
var ranks = [],
i,
j,
fen = "";
board.pieces.forEach(function (piece)
{
if (!piece.captured) {
if (!ranks[piece.rank]) {
ranks[piece.rank] = [];
}
ranks[piece.rank][piece.file] = piece.type;
if (piece.color === "w") {
ranks[piece.rank][piece.file] = ranks[piece.rank][piece.file].toUpperCase();
}
}
});
/// Start with the last rank.
for (i = board_details.ranks - 1; i >= 0; i -= 1) {
if (ranks[i]) {
for (j = 0; j < board_details.files; j += 1) {
if (ranks[i][j]) {
fen += ranks[i][j];
} else {
fen += "1";
}
}
} else {
fen += "8";
}
if (i > 0) {
fen += "/";
}
}
/// Replace 1's with their number (e.g., 11 with 2).
fen = fen.replace(/1{2,}/g, function replacer(ones)
{
return String(ones.length);
});
if (full) {
return fen + " " + board.turn;
}
return fen;
}
function find_king(color)
{
var i;
for (i = board.pieces.length - 1; i >= 0; i -= 1) {
if (board.pieces[i].color === color && board.pieces[i].type === "k") {
return board.pieces[i];
}
}
}
function focus_checked_king(king)
{
if (king) {
focus_square(king.file, king.rank, "red");
}
board.checked_king = king;
}
function show_lines_of_power()
{
var power_squares = [];
function add_square(rank, file, piece)
{
var color;
if (rank >= 0 && rank < board_details.ranks && file >= 0 && file < board_details.files) {
if (!power_squares[rank]) {
power_squares[rank] = [];
}
color = piece.color === "w" ? "red" : "blue";
/// Mix.
if (power_squares[rank][file] && power_squares[rank][file].color !== color) {
color = "purple";
}
power_squares[rank][file] = {rank: rank, file: file, color: color};
///TODO: Remove squares when in check that do not remove check.
}
}
function add_squares_dir(piece, file_change, rank_change)
{
var rank = piece.rank,
file = piece.file;
for (;;) {
rank += rank_change;
file += file_change;
if (file >= 0 && file < board_details.files && rank >= 0 && rank < board_details.ranks) {
add_square(rank, file, piece);
/// Stop at a piece (either friend or foe)
if (get_piece_from_rank_file(rank, file)) {
break;
}
} else {
break;
}
}
}
function add_diagonal_squares(piece)
{
add_squares_dir(piece, 1, 1);
add_squares_dir(piece, 1, -1);
add_squares_dir(piece, -1, 1);
add_squares_dir(piece, -1, -1);
}
function add_orthogonal_squares(piece)
{
add_squares_dir(piece, 1, 0);
add_squares_dir(piece, -1, 0);
add_squares_dir(piece, 0, 1);
add_squares_dir(piece, 0, -1);
}
board.pieces.forEach(function oneach(piece)
{
var dir;
if (!piece.captured) {
if (piece.type === "p") {
if (piece.color === "w") {
dir = 1;
} else {
dir = -1;
}
add_square(piece.rank + dir, piece.file + 1, piece);
add_square(piece.rank + dir, piece.file - 1, piece);
} else if (piece.type === "n") {
add_square(piece.rank + 2, piece.file + 1, piece);
add_square(piece.rank - 2, piece.file + 1, piece);
add_square(piece.rank + 1, piece.file + 2, piece);
add_square(piece.rank + 1, piece.file - 2, piece);
add_square(piece.rank - 2, piece.file - 1, piece);
add_square(piece.rank + 2, piece.file - 1, piece);
add_square(piece.rank - 1, piece.file - 2, piece);
add_square(piece.rank - 1, piece.file + 2, piece);
} else if (piece.type === "b") {
add_diagonal_squares(piece);
} else if (piece.type === "r") {
add_orthogonal_squares(piece);
} else if (piece.type === "q") {
add_orthogonal_squares(piece);
add_diagonal_squares(piece);
} else if (piece.type === "k") {
add_square(piece.rank + 1, piece.file + 1, piece);
add_square(piece.rank - 1, piece.file - 1, piece);
add_square(piece.rank + 1, piece.file - 1, piece);
add_square(piece.rank - 1, piece.file + 1, piece);
add_square(piece.rank + 1, piece.file , piece);
add_square(piece.rank - 1, piece.file , piece);
add_square(piece.rank , piece.file - 1, piece);
add_square(piece.rank , piece.file + 1, piece);
}
}
});
power_squares.forEach(function oneach(ranks)
{
ranks.forEach(function oneach(data)
{
highlight_square(data.rank, data.file, data.color);
});
});
}
function set_legal_moves(moves)
{
legal_moves = moves;
if (board.display_lines_of_power) {
show_lines_of_power();
}
G.events.trigger("board_set_legal_moves", {moves: moves});
}
function get_legal_moves()
{
return legal_moves;
}
function check_highlight(e)
{
var king;
if (legal_moves && legal_moves.checkers && legal_moves.checkers.length) {
king = find_king(board.turn);
legal_moves.checkers.forEach(function (checker)
{
var checker_data = get_rank_file_from_str(checker);
arrow_manager.draw(checker_data.rank, checker_data.file, king.rank, king.file, rgba[1], true);
});
}
///NOTE: This will clear the checked king square if there is no checked king, so it must always be called.
focus_checked_king(king);
}
function get_mode()
{
return mode;
}
function set_mode(new_mode)
{
var old_mode = mode;
if ((new_mode === "play" || new_mode === "setup") && typeof board.close_modular_window === "function") {
board.close_modular_window();
}
mode = new_mode;
G.events.trigger("board_mode_change", {old_move: old_mode, mode: new_mode});
}
function monitor_mode_change(e)
{
if (e.mode === "setup") {
clear_board_extras();
}
}
function fixHiDPI(context) {
var backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
var scaleVal = (window.devicePixelRatio || 1) / backingStore;
console.log(scaleVal);
context.scale(scaleVal, scaleVal);
return context;
}
///
/// Start creating board
///
options = options || {};
G.events.attach("board_set_legal_moves", check_highlight);
arrow_manager = (function create_draw_arrow()
{
var canvas = document.createElement("canvas"),
ctx,
on_dom,
arrows = [],
canvas_left,
canvas_top,
remove_timer;
function get_intersect(x1, y1, x2, y2, x3, y3, x4, y4)
{
/// See https://en.wikipedia.org/wiki/Lineline_intersection.
return {
x: ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)),
y: ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
};
}
function rotate_point(point_x, point_y, origin_x, origin_y, angle) {
return {
x: Math.cos(angle) * (point_x - origin_x) - Math.sin(angle) * (point_y - origin_y) + origin_x,
y: Math.sin(angle) * (point_x - origin_x) + Math.cos(angle) * (point_y - origin_y) + origin_y
};
}
function create_arrow(x1, y1, x2, y2, options)
{
options = options || {};
options.width = options.width || 12;
options.fillStyle = options.fillStyle || "rgb(0,0,200)";
options.head_len = options.head_len || 30;
if (options.head_len < options.width + 1) {
options.head_len = options.width + 1;
}
options.head_angle = options.head_angle || Math.PI / 6;
var angle = Math.atan2(y2 - y1, x2 - x1);
var ang_neg = angle - options.head_angle;
var ang_pos = angle + options.head_angle;
var tri_point1 = {
x: x2 - options.head_len * Math.cos(ang_neg),
y: y2 - options.head_len * Math.sin(ang_neg)
};
var tri_point2 = {
x: x2 - options.head_len * Math.cos(ang_pos),
y: y2 - options.head_len * Math.sin(ang_pos)
};
/// Since the line has a width, we need to create a new line by moving the point half of the width and then rotating it to match the line.
var p1 = rotate_point(x1, y1 + options.width / 2, x1, y1, angle);
var p2 = rotate_point(x2, y2 + options.width / 2, x2, y2, angle);
/// Find the point at which the line will reach the bottom of the triangle.
var int2 = get_intersect(p1.x, p1.y, p2.x, p2.y, tri_point1.x, tri_point1.y, tri_point2.x, tri_point2.y);
var p3 = rotate_point(x1, y1 - options.width / 2, x1, y1, angle);
var p4 = rotate_point(x2, y2 - options.width / 2, x2, y2, angle);
var int3 = get_intersect(p3.x, p3.y, p4.x, p4.y, tri_point1.x, tri_point1.y, tri_point2.x, tri_point2.y);
ctx.fillStyle = options.fillStyle;
ctx.beginPath();
ctx.arc(x1, y1, options.width / 2, angle - Math.PI / 2, angle - Math.PI * 1.5, true);
ctx.lineTo(int2.x, int2.y);
ctx.lineTo(tri_point1.x, tri_point1.y);
ctx.lineTo(x2, y2);
ctx.lineTo(tri_point2.x, tri_point2.y);
ctx.lineTo(int3.x, int3.y);
ctx.closePath();
if (options.lineWidth) {
ctx.lineWidth = options.lineWidth;
ctx.strokeStyle = options.strokeStyle;
ctx.stroke();
}
ctx.fill();
}
function draw_arrow(rank1, file1, rank2, file2, color, auto, do_not_add)
{
var box1 = squares[rank1][file1].getBoundingClientRect(),
box2 = squares[rank2][file2].getBoundingClientRect(),
proportion,
adjust_height;
if (!do_not_add) {
arrows.push({
rank1: rank1,
file1: file1,
rank2: rank2,
file2: file2,
color: color,
auto: auto,
});
}
if (!on_dom) {
set_size();
document.body.appendChild(canvas);
on_dom = true;
}
proportion = (box1.width / 50);
create_arrow(window.scrollX + box1.left + box1.width / 2 - canvas_left, window.scrollY + box1.top + box1.height / 2 - canvas_top,
window.scrollX + box2.left + box2.width / 2 - canvas_left, window.scrollY + box2.top + box2.height / 2 - canvas_top,
{
fillStyle: color,
width: box1.width / 5,
head_len: box1.width / 1.5,
///lineWidth: box1.width / 10,
///strokeStyle: "rgba(200,200,200,.4)",
});
return do_not_add ? -1 : arrows.length - 1;
}
function remove_if_empty()
{
clearTimeout(remove_timer);
/// Since we often draw another arrow quickly, there's no need to remove it right away.
remove_timer = setTimeout(function ()
{
if (on_dom && !arrows.length) {
if (canvas.parentNode) {
canvas.parentNode.removeChild(canvas);
}
on_dom = false;
}
}, 2000);
}
function clear(keep_auto_arrows)
{
var i;
/// Sometimes, we don't want to remove the arrows for last move and checkers.
if (keep_auto_arrows) {
for (i = arrows.length - 1; i >= 0; i -= 1) {
if (!arrows[i].auto) {
G.array_remove(arrows, i);
}
}
} else {
arrows = [];
}
set_size();
if (arrows.length) {
draw_all_arrows();
} else {
remove_if_empty();
}
}
function draw_all_arrows()
{
arrows.forEach(function (arrow)
{
draw_arrow(arrow.rank1, arrow.file1, arrow.rank2, arrow.file2, arrow.color, arrow.auto, true);
});
}
function set_size()
{
var box = board.el.getBoundingClientRect();
canvas_left = box.left + window.scrollX;
canvas_top = box.top + window.scrollY;
canvas.width = box.width;
canvas.height = box.height;
canvas.style.top = canvas_top + "px";
canvas.style.left = canvas_left + "px";
}
function redraw()
{
set_size();
draw_all_arrows();
}
function delete_arrow(which)
{
if (!which) {
which = arrows.length - 1;
for (;;) {
/// We are looking for the last arrow drawn by the user.
if (arrows[which] && !arrows[which].auto) {
break;
}
which -= 1;
if (which < 0) {
return;
}
}
}
if (arrows[which]) {
G.array_remove(arrows, which);
redraw();
}
remove_if_empty();
}
function arrow_onmove(e)
{
var uci_data = split_uci(e.uci),
rook_data;
draw_arrow(uci_data.starting.rank, uci_data.starting.file, uci_data.ending.rank, uci_data.ending.file, rgba[0], true);
/// Draw rook arrow on castling.
if (e.san === "O-O" || e.san === "0-0") {
rook_data = {
start_file: board_details.files - 1,
end_file: uci_data.ending.file - 1
};
} else if (e.san === "O-O-O" || e.san === "0-0-0") {
rook_data = {
start_file: 0,
end_file: uci_data.ending.file + 1
};
}
if (rook_data) {
draw_arrow(uci_data.starting.rank, rook_data.start_file, uci_data.ending.rank, rook_data.end_file, rook_arrow_color, true);
}
}
function draw(rank1, file1, rank2, file2, color, auto)
{
return draw_arrow(rank1, file1, rank2, file2, color, auto);
}
G.events.attach("board_resize", function redrawDelayed()
{
setTimeout(redraw);
});
G.events.attach("board_move", arrow_onmove);
canvas.className = "boardArrows";
ctx = fixHiDPI(canvas.getContext("2d"));
return {
el: canvas,
draw: draw,
clear: clear,
delete_arrow: delete_arrow,
arrow_onmove: arrow_onmove,
};
}());
function clear()
{
clear_highlights()
set_board("8/8/8/8/8/8/8/8 w - - 0 1");
}
board = {
pieces: [],
size_board: size_board,
pieces_path: typeof options.pieces_path === "undefined" ? "img/pieces/" : options.pieces_path,
theme: typeof options.theme === "undefined" ? "default" : options.theme,
wait: wait,
play: play,
enable_setup: enable_setup,
move: move,
players: {
w: {
color: "w",
},
b: {
color: "b",
}
},
switch_turn: switch_turn,
set_board: set_board,
is_legal_move: is_legal_move,
moves: [],
get_fen: get_fen,
board_details: board_details,
highlight_colors: colors,
color_values: rgba,
clear_highlights: clear_highlights,
remove_highlight: remove_highlight,
highlight_square: highlight_square,
set_legal_moves: set_legal_moves,
get_legal_moves: get_legal_moves,
show_lines_of_power: show_lines_of_power,
get_mode: get_mode,
set_mode: set_mode,
get_san: get_san,
create_modular_window: create_modular_window,
arrow_manager: arrow_manager,
split_uci: split_uci,
add_piece: add_piece,
clear: clear,
clear_board_extras: clear_board_extras,
select_piece: select_piece,
flip: flip,
/// onmove()
/// onswitch()
/// turn
/// display_lines_of_power
};
G.events.attach("board_mode_change", monitor_mode_change);
create_board(el, options.dim);
set_board(options.pos);
window.addEventListener("mousemove", onmousemove);
window.addEventListener("touchmove", onmousemove);
window.addEventListener("mouseup", onmouseup);
window.addEventListener("touchend", onmouseup);
window.addEventListener("keydown", onkeydown);
window.addEventListener("keyup", onkeyup);
return board;
};