/* 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/Line–line_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; };