+ get pawnPromotions(x, y) {
+ const base_pieces = ['q', 'r', 'n', 'b', 'j', 's'];
+ let lancers = [];
+ if (y > 0)
+ lancers.push('m');
+ if (y < this.size.y)
+ lancers.push('e');
+ if (x == 0) {
+ lancers.push('g');
+ if (y > 0)
+ lancers.push('h');
+ if (y < this.size.y)
+ lancers.push('f');
+ }
+ else { //x == this.size.x (8)
+ lancers.push('c');
+ if (y > 0)
+ lancers.push('o');
+ if (y < this.size.y)
+ lancers.push('d');
+ }
+ return ['q', 'r', 'n', 'b', 'j', 's', 'l'];
+ }
+
+ // Setup the initial random-or-not (asymmetric-or-not) position
+ genRandInitBaseFen() {
+ const s = FenUtil.setupPieces(
+ ['j', 'l', 's', 'q', 'k', 'b', 'n', 'r'],
+ {
+ randomness: this.options["randomness"],
+ between: [{p1: 'k', p2: ['r', 'j']}],
+ diffCol: ['bs'],
+ range: {'s': [2, 3, 4, 5]},
+ flags: ['r', 'j']
+ }
+ );
+ return {
+ fen: s.b.join("") + "/pppppppp/8/8/8/8/PPPPPPPP/" +
+ s.w.join("").toUpperCase(),
+ o: {flags: s.flags}
+ };
+ }
+
+
+
+// TODO: from here
+
+
+
+ //////////////////
+ // INITIALIZATION
+
+ constructor(o) {
+ this.options = o.options;
+ // Fill missing options (always the case if random challenge)
+ (V.Options.select || []).concat(V.Options.input || []).forEach(opt => {
+ if (this.options[opt.variable] === undefined)
+ this.options[opt.variable] = opt.defaut;
+ });
+
+ // Some variables
+ this.playerColor = o.color;
+ this.afterPlay = o.afterPlay; //trigger some actions after playing a move
+ this.containerId = o.element;
+ this.isDiagram = o.diagram;
+ this.marks = o.marks;
+
+ // Initializations
+ if (!o.fen)
+ o.fen = this.genRandInitFen(o.seed);
+ this.re_initFromFen(o.fen);
+ this.graphicalInit();
+ }
+
+ re_initFromFen(fen, oldBoard) {
+ const fenParsed = this.parseFen(fen);
+ this.board = oldBoard || this.getBoard(fenParsed.position);
+ this.turn = fenParsed.turn;
+ this.movesCount = parseInt(fenParsed.movesCount, 10);
+ this.setOtherVariables(fenParsed);
+ }
+
+ // Turn position fen into double array ["wb","wp","bk",...]
+ getBoard(position) {
+ const rows = position.split("/");
+ let board = ArrayFun.init(this.size.x, this.size.y, "");
+ for (let i = 0; i < rows.length; i++) {
+ let j = 0;
+ for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
+ const character = rows[i][indexInRow];
+ const num = parseInt(character, 10);
+ // If num is a number, just shift j:
+ if (!isNaN(num))
+ j += num;
+ // Else: something at position i,j
+ else
+ board[i][j++] = this.fen2board(character);
+ }
+ }
+ return board;
+ }
+
+ // Some additional variables from FEN (variant dependant)
+ setOtherVariables(fenParsed) {
+ // Set flags and enpassant:
+ if (this.hasFlags)
+ this.setFlags(fenParsed.flags);
+ if (this.hasEnpassant)
+ this.epSquare = this.getEpSquare(fenParsed.enpassant);
+ if (this.hasReserve && !this.isDiagram)
+ this.initReserves(fenParsed.reserve);
+ if (this.options["crazyhouse"])
+ this.initIspawn(fenParsed.ispawn);
+ if (this.options["teleport"]) {
+ this.subTurnTeleport = 1;
+ this.captured = null;
+ }
+ if (this.options["dark"]) {
+ // Setup enlightened: squares reachable by player side
+ this.enlightened = ArrayFun.init(this.size.x, this.size.y, false);
+ this.updateEnlightened();
+ }
+ this.subTurn = 1; //may be unused
+ if (!this.moveStack) //avoid resetting (unwanted)
+ this.moveStack = [];
+ }
+
+ // ordering as in pieces() p,r,n,b,q,k
+ static get ReserveArray() {
+ return ['p', 'r', 'n', 'b', 'q', 'k'];
+ }
+
+ initReserves(reserveStr) {
+ const counts = reserveStr.split("").map(c => parseInt(c, 36));
+ const L = V.ReserveArray.length;
+ this.reserve = {
+ w: ArrayFun.toObject(V.ReserveArray, counts.slice(0, L)),
+ b: ArrayFun.toObject(V.ReserveArray, counts.slice(L, 2 * L))
+ };
+ }
+
+ initIspawn(ispawnStr) {
+ if (ispawnStr != "-")
+ this.ispawn = ArrayFun.toObject(ispawnStr.split(","), true);
+ else
+ this.ispawn = {};
+ }
+
+ ////////////////
+ // VISUAL UTILS
+
+ getPieceWidth(rwidth) {
+ return (rwidth / Math.max(this.size.x, this.size.y));
+ }
+
+ getReserveSquareSize(rwidth, nbR) {
+ const sqSize = this.getPieceWidth(rwidth);
+ return Math.min(sqSize, rwidth / nbR);
+ }
+
+ getReserveNumId(color, piece) {
+ return `${this.containerId}|rnum-${color}${piece}`;
+ }
+
+ getNbReservePieces(color) {
+ return (
+ Object.values(this.reserve[color]).reduce(
+ (oldV,newV) => oldV + (newV > 0 ? 1 : 0), 0)
+ );
+ }
+
+ getRankInReserve(c, p) {
+ const pieces = Object.keys(this.pieces(c, c, p));
+ const lastIndex = pieces.findIndex(pp => pp == p)
+ let toTest = pieces.slice(0, lastIndex);
+ return toTest.reduce(
+ (oldV,newV) => oldV + (this.reserve[c][newV] > 0 ? 1 : 0), 0);
+ }
+
+ static AddClass_es(elt, class_es) {
+ if (!Array.isArray(class_es))
+ class_es = [class_es];
+ class_es.forEach(cl => elt.classList.add(cl));
+ }
+
+ static RemoveClass_es(elt, class_es) {
+ if (!Array.isArray(class_es))
+ class_es = [class_es];
+ class_es.forEach(cl => elt.classList.remove(cl));
+ }
+
+ // Generally light square bottom-right
+ getSquareColorClass(x, y) {
+ return ((x+y) % 2 == 0 ? "light-square": "dark-square");
+ }
+
+ getMaxDistance(r) {
+ // Works for all rectangular boards:
+ return Math.sqrt(r.width ** 2 + r.height ** 2);
+ }
+
+ getDomPiece(x, y) {
+ return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y];
+ }
+
+ //////////////////
+ // VISUAL METHODS
+
+ graphicalInit() {
+ const g_init = () => {
+ this.re_drawBoardElements();
+ if (!this.isDiagram && !this.mouseListeners && !this.touchListeners)
+ this.initMouseEvents();
+ };
+ let container = document.getElementById(this.containerId);
+ this.windowResizeObs = new ResizeObserver(g_init);
+ this.windowResizeObs.observe(container);
+ }
+
+ re_drawBoardElements() {
+ const board = this.getSvgChessboard();
+ const container = document.getElementById(this.containerId);
+ const rc = container.getBoundingClientRect();
+ let chessboard = container.querySelector(".chessboard");
+ chessboard.innerHTML = "";
+ chessboard.insertAdjacentHTML('beforeend', board);
+ // Compare window ratio width / height to aspectRatio:
+ const windowRatio = rc.width / rc.height;
+ let cbWidth, cbHeight;
+ const vRatio = this.size.ratio || 1;
+ if (windowRatio <= vRatio) {
+ // Limiting dimension is width:
+ cbWidth = Math.min(rc.width, 767);
+ cbHeight = cbWidth / vRatio;
+ }
+ else {
+ // Limiting dimension is height:
+ cbHeight = Math.min(rc.height, 767);
+ cbWidth = cbHeight * vRatio;
+ }
+ if (this.hasReserve && !this.isDiagram) {
+ const sqSize = cbWidth / this.size.y;
+ // NOTE: allocate space for reserves (up/down) even if they are empty
+ // Cannot use getReserveSquareSize() here, but sqSize is an upper bound.
+ if ((rc.height - cbHeight) / 2 < sqSize + 5) {
+ cbHeight = rc.height - 2 * (sqSize + 5);
+ cbWidth = cbHeight * vRatio;
+ }
+ }
+ chessboard.style.width = cbWidth + "px";
+ chessboard.style.height = cbHeight + "px";
+ // Center chessboard:
+ const spaceLeft = (rc.width - cbWidth) / 2,
+ spaceTop = (rc.height - cbHeight) / 2;
+ chessboard.style.left = spaceLeft + "px";
+ chessboard.style.top = spaceTop + "px";
+ // Give sizes instead of recomputing them,
+ // because chessboard might not be drawn yet.
+ this.setupVisualPieces({
+ width: cbWidth,
+ height: cbHeight,
+ x: spaceLeft,
+ y: spaceTop
+ });
+ }
+
+ // Get SVG board (background, no pieces)
+ getSvgChessboard() {
+ let board = `
+ <svg
+ viewBox="0 0 ${10*this.size.y} ${10*this.size.x}"
+ class="chessboard_SVG">`;
+ board += this.getBaseSvgChessboard();
+ board += "</svg>";
+ return board;
+ }
+
+ getBaseSvgChessboard() {
+ let board = "";
+ const flipped = this.flippedBoard;
+ for (let i=0; i < this.size.x; i++) {
+ for (let j=0; j < this.size.y; j++) {
+ if (!this.onBoard(i, j))
+ continue;
+ const ii = (flipped ? this.size.x - 1 - i : i);
+ const jj = (flipped ? this.size.y - 1 - j : j);
+ let classes = this.getSquareColorClass(ii, jj);
+ if (this.enlightened && !this.enlightened[ii][jj])
+ classes += " in-shadow";
+ // NOTE: x / y reversed because coordinates system is reversed.
+ board += `
+ <rect
+ class="${classes}"
+ id="${this.coordsToId({x: ii, y: jj})}"
+ width="10"
+ height="10"
+ x="${10*j}"
+ y="${10*i}"
+ />`;
+ }
+ }
+ return board;
+ }
+
+ setupVisualPieces(r) {
+ let chessboard =
+ document.getElementById(this.containerId).querySelector(".chessboard");
+ if (!r)
+ r = chessboard.getBoundingClientRect();
+ const pieceWidth = this.getPieceWidth(r.width);
+ const addPiece = (i, j, arrName, classes) => {
+ this[arrName][i][j] = document.createElement("piece");
+ C.AddClass_es(this[arrName][i][j], classes);
+ this[arrName][i][j].style.width = pieceWidth + "px";
+ this[arrName][i][j].style.height = pieceWidth + "px";
+ let [ip, jp] = this.getPixelPosition(i, j, r);
+ // Translate coordinates to use chessboard as reference:
+ this[arrName][i][j].style.transform =
+ `translate(${ip - r.x}px,${jp - r.y}px)`;
+ chessboard.appendChild(this[arrName][i][j]);
+ };
+ const conditionalReset = (arrName) => {
+ if (this[arrName]) {
+ // Refreshing: delete old pieces first. This isn't necessary,
+ // but simpler (this method isn't called many times)
+ for (let i=0; i<this.size.x; i++) {
+ for (let j=0; j<this.size.y; j++) {
+ if (this[arrName][i][j]) {
+ this[arrName][i][j].remove();
+ this[arrName][i][j] = null;
+ }
+ }
+ }
+ }
+ else
+ this[arrName] = ArrayFun.init(this.size.x, this.size.y, null);
+ if (arrName == "d_pieces")
+ this.marks.forEach((m) => {
+ const mCoords = this.coordsFromUsual(m);
+ addPiece(mCoords.x, mCoords.y, arrName, "mark");
+ });
+ };
+ if (this.marks)
+ conditionalReset("d_pieces");
+ conditionalReset("g_pieces");
+ for (let i=0; i < this.size.x; i++) {
+ for (let j=0; j < this.size.y; j++) {
+ if (this.board[i][j] != "") {
+ const color = this.getColor(i, j);
+ const piece = this.getPiece(i, j);
+ addPiece(i, j, "g_pieces", this.pieces(color, i, j)[piece]["class"]);
+ this.g_pieces[i][j].classList.add(V.GetColorClass(color));
+ if (this.enlightened && !this.enlightened[i][j])
+ this.g_pieces[i][j].classList.add("hidden");
+ }
+ if (this.marks && this.d_pieces[i][j]) {
+ let classes = ["mark"];
+ if (this.board[i][j] != "")
+ classes.push("transparent");
+ addPiece(i, j, "d_pieces", classes);
+ }
+ }
+ }
+ if (this.hasReserve && !this.isDiagram)
+ this.re_drawReserve(['w', 'b'], r);
+ }
+
+ // NOTE: assume this.reserve != null
+ re_drawReserve(colors, r) {
+ if (this.r_pieces) {
+ // Remove (old) reserve pieces
+ for (let c of colors) {
+ Object.keys(this.r_pieces[c]).forEach(p => {
+ this.r_pieces[c][p].remove();
+ delete this.r_pieces[c][p];
+ const numId = this.getReserveNumId(c, p);
+ document.getElementById(numId).remove();
+ });
+ }
+ }
+ else
+ this.r_pieces = { w: {}, b: {} };
+ let container = document.getElementById(this.containerId);
+ if (!r)
+ r = container.querySelector(".chessboard").getBoundingClientRect();
+ for (let c of colors) {
+ let reservesDiv = document.getElementById("reserves_" + c);
+ if (reservesDiv)
+ reservesDiv.remove();
+ if (!this.reserve[c])
+ continue;
+ const nbR = this.getNbReservePieces(c);
+ if (nbR == 0)
+ continue;
+ const sqResSize = this.getReserveSquareSize(r.width, nbR);
+ let ridx = 0;
+ const vShift = (c == this.playerColor ? r.height + 5 : -sqResSize - 5);
+ const [i0, j0] = [r.x, r.y + vShift];
+ let rcontainer = document.createElement("div");
+ rcontainer.id = "reserves_" + c;
+ rcontainer.classList.add("reserves");
+ rcontainer.style.left = i0 + "px";
+ rcontainer.style.top = j0 + "px";
+ // NOTE: +1 fix display bug on Firefox at least
+ rcontainer.style.width = (nbR * sqResSize + 1) + "px";
+ rcontainer.style.height = sqResSize + "px";
+ container.appendChild(rcontainer);
+ for (let p of Object.keys(this.reserve[c])) {
+ if (this.reserve[c][p] == 0)
+ continue;
+ let r_cell = document.createElement("div");
+ r_cell.id = this.coordsToId({x: c, y: p});
+ r_cell.classList.add("reserve-cell");
+ r_cell.style.width = sqResSize + "px";
+ r_cell.style.height = sqResSize + "px";
+ rcontainer.appendChild(r_cell);
+ let piece = document.createElement("piece");
+ C.AddClass_es(piece, this.pieces(c, c, p)[p]["class"]);
+ piece.classList.add(V.GetColorClass(c));
+ piece.style.width = "100%";
+ piece.style.height = "100%";
+ this.r_pieces[c][p] = piece;
+ r_cell.appendChild(piece);
+ let number = document.createElement("div");
+ number.textContent = this.reserve[c][p];
+ number.classList.add("reserve-num");
+ number.id = this.getReserveNumId(c, p);
+ const fontSize = "1.3em";
+ number.style.fontSize = fontSize;
+ number.style.fontSize = fontSize;
+ r_cell.appendChild(number);
+ ridx++;
+ }
+ }
+ }
+
+ updateReserve(color, piece, count) {
+ if (this.options["cannibal"] && C.CannibalKings[piece])
+ piece = "k"; //capturing cannibal king: back to king form
+ const oldCount = this.reserve[color][piece];
+ this.reserve[color][piece] = count;
+ // Redrawing is much easier if count==0 (or undefined)
+ if ([oldCount, count].some(item => !item))
+ this.re_drawReserve([color]);
+ else {
+ const numId = this.getReserveNumId(color, piece);
+ document.getElementById(numId).textContent = count;
+ }
+ }
+
+ // Resize board: no need to destroy/recreate pieces
+ rescale(mode) {
+ const container = document.getElementById(this.containerId);
+ let chessboard = container.querySelector(".chessboard");
+ const rc = container.getBoundingClientRect(),
+ r = chessboard.getBoundingClientRect();
+ const multFact = (mode == "up" ? 1.05 : 0.95);
+ let [newWidth, newHeight] = [multFact * r.width, multFact * r.height];
+ // Stay in window:
+ const vRatio = this.size.ratio || 1;
+ if (newWidth > rc.width) {
+ newWidth = rc.width;
+ newHeight = newWidth / vRatio;
+ }
+ if (newHeight > rc.height) {
+ newHeight = rc.height;
+ newWidth = newHeight * vRatio;
+ }
+ chessboard.style.width = newWidth + "px";
+ chessboard.style.height = newHeight + "px";
+ const newX = (rc.width - newWidth) / 2;
+ chessboard.style.left = newX + "px";
+ const newY = (rc.height - newHeight) / 2;
+ chessboard.style.top = newY + "px";
+ const newR = {x: newX, y: newY, width: newWidth, height: newHeight};
+ const pieceWidth = this.getPieceWidth(newWidth);
+ // NOTE: next "if" for variants which use squares filling
+ // instead of "physical", moving pieces
+ if (this.g_pieces) {
+ for (let i=0; i < this.size.x; i++) {
+ for (let j=0; j < this.size.y; j++) {
+ if (this.g_pieces[i][j]) {
+ // NOTE: could also use CSS transform "scale"
+ this.g_pieces[i][j].style.width = pieceWidth + "px";
+ this.g_pieces[i][j].style.height = pieceWidth + "px";
+ const [ip, jp] = this.getPixelPosition(i, j, newR);
+ // Translate coordinates to use chessboard as reference:
+ this.g_pieces[i][j].style.transform =
+ `translate(${ip - newX}px,${jp - newY}px)`;
+ }
+ }
+ }
+ }
+ if (this.hasReserve)
+ this.rescaleReserve(newR);
+ }
+
+ rescaleReserve(r) {
+ for (let c of ['w','b']) {
+ if (!this.reserve[c])
+ continue;
+ const nbR = this.getNbReservePieces(c);
+ if (nbR == 0)
+ continue;
+ // Resize container first
+ const sqResSize = this.getReserveSquareSize(r.width, nbR);
+ const vShift = (c == this.playerColor ? r.height + 5 : -sqResSize - 5);
+ const [i0, j0] = [r.x, r.y + vShift];
+ let rcontainer = document.getElementById("reserves_" + c);
+ rcontainer.style.left = i0 + "px";
+ rcontainer.style.top = j0 + "px";
+ rcontainer.style.width = (nbR * sqResSize + 1) + "px";
+ rcontainer.style.height = sqResSize + "px";
+ // And then reserve cells:
+ const rpieceWidth = this.getReserveSquareSize(r.width, nbR);
+ Object.keys(this.reserve[c]).forEach(p => {
+ if (this.reserve[c][p] == 0)
+ return;
+ let r_cell = document.getElementById(this.coordsToId({x: c, y: p}));
+ r_cell.style.width = sqResSize + "px";
+ r_cell.style.height = sqResSize + "px";
+ });
+ }
+ }
+
+ // Return the absolute pixel coordinates given current position.
+ // Our coordinate system differs from CSS one (x <--> y).
+ // We return here the CSS coordinates (more useful).
+ getPixelPosition(i, j, r) {
+ if (i < 0 || j < 0)
+ return [0, 0]; //piece vanishes
+ let x, y;
+ if (typeof i == "string") {
+ // Reserves: need to know the rank of piece
+ const nbR = this.getNbReservePieces(i);
+ const rsqSize = this.getReserveSquareSize(r.width, nbR);
+ x = this.getRankInReserve(i, j) * rsqSize;
+ y = (this.playerColor == i ? y = r.height + 5 : - 5 - rsqSize);
+ }
+ else {
+ const sqSize = r.width / Math.max(this.size.x, this.size.y);
+ const flipped = this.flippedBoard;
+ x = (flipped ? this.size.y - 1 - j : j) * sqSize +
+ Math.abs(this.size.x - this.size.y) * sqSize / 2;
+ y = (flipped ? this.size.x - 1 - i : i) * sqSize;
+ }
+ return [r.x + x, r.y + y];
+ }
+
+ initMouseEvents() {
+ let container = document.getElementById(this.containerId);
+ let chessboard = container.querySelector(".chessboard");
+
+ const getOffset = e => {
+ if (e.clientX)
+ // Mouse
+ return {x: e.clientX, y: e.clientY};
+ let touchLocation = null;
+ if (e.targetTouches && e.targetTouches.length >= 1)
+ // Touch screen, dragstart
+ touchLocation = e.targetTouches[0];
+ else if (e.changedTouches && e.changedTouches.length >= 1)
+ // Touch screen, dragend
+ touchLocation = e.changedTouches[0];
+ if (touchLocation)
+ return {x: touchLocation.clientX, y: touchLocation.clientY};
+ return {x: 0, y: 0}; //shouldn't reach here =)
+ }
+
+ const centerOnCursor = (piece, e) => {
+ const centerShift = this.getPieceWidth(r.width) / 2;
+ const offset = getOffset(e);
+ piece.style.left = (offset.x - centerShift) + "px";
+ piece.style.top = (offset.y - centerShift) + "px";
+ }
+
+ let start = null,
+ r = null,
+ startPiece, curPiece = null,
+ pieceWidth;
+ const mousedown = (e) => {
+ // Disable zoom on smartphones:
+ if (e.touches && e.touches.length > 1)
+ e.preventDefault();
+ r = chessboard.getBoundingClientRect();
+ pieceWidth = this.getPieceWidth(r.width);
+ const cd = this.idToCoords(e.target.id);
+ if (cd) {
+ const move = this.doClick(cd);
+ if (move)
+ this.buildMoveStack(move, r);
+ else if (!this.clickOnly) {
+ const [x, y] = Object.values(cd);
+ if (typeof x != "number")
+ startPiece = this.r_pieces[x][y];
+ else
+ startPiece = this.g_pieces[x][y];
+ if (startPiece && this.canIplay(x, y)) {
+ e.preventDefault();
+ start = cd;
+ curPiece = startPiece.cloneNode();
+ curPiece.style.transform = "none";
+ curPiece.style.zIndex = 5;
+ curPiece.style.width = pieceWidth + "px";
+ curPiece.style.height = pieceWidth + "px";
+ centerOnCursor(curPiece, e);
+ container.appendChild(curPiece);
+ startPiece.style.opacity = "0.4";
+ chessboard.style.cursor = "none";
+ }
+ }
+ }
+ };
+
+ const mousemove = (e) => {
+ if (start) {
+ e.preventDefault();
+ centerOnCursor(curPiece, e);
+ }
+ else if (e.changedTouches && e.changedTouches.length >= 1)
+ // Attempt to prevent horizontal swipe...
+ e.preventDefault();
+ };
+
+ const mouseup = (e) => {
+ if (!start)
+ return;
+ const [x, y] = [start.x, start.y];
+ start = null;
+ e.preventDefault();
+ chessboard.style.cursor = "pointer";
+ startPiece.style.opacity = "1";
+ const offset = getOffset(e);
+ const landingElt = document.elementFromPoint(offset.x, offset.y);
+ const cd =
+ (landingElt ? this.idToCoords(landingElt.id) : undefined);
+ if (cd) {
+ // NOTE: clearly suboptimal, but much easier, and not a big deal.
+ const potentialMoves = this.getPotentialMovesFrom([x, y])
+ .filter(m => m.end.x == cd.x && m.end.y == cd.y);
+ const moves = this.filterValid(potentialMoves);
+ if (moves.length >= 2)
+ this.showChoices(moves, r);
+ else if (moves.length == 1)
+ this.buildMoveStack(moves[0], r);
+ }
+ curPiece.remove();
+ };
+
+ const resize = (e) => this.rescale(e.deltaY < 0 ? "up" : "down");
+
+ if ('onmousedown' in window) {
+ this.mouseListeners = [
+ {type: "mousedown", listener: mousedown},
+ {type: "mousemove", listener: mousemove},
+ {type: "mouseup", listener: mouseup},
+ {type: "wheel", listener: resize}
+ ];
+ this.mouseListeners.forEach(ml => {
+ document.addEventListener(ml.type, ml.listener);
+ });
+ }
+ if ('ontouchstart' in window) {
+ this.touchListeners = [
+ {type: "touchstart", listener: mousedown},
+ {type: "touchmove", listener: mousemove},
+ {type: "touchend", listener: mouseup}
+ ];
+ this.touchListeners.forEach(tl => {
+ // https://stackoverflow.com/a/42509310/12660887
+ document.addEventListener(tl.type, tl.listener, {passive: false});
+ });
+ }
+ // TODO: onpointerdown/move/up ? See reveal.js /controllers/touch.js
+ }
+
+ // NOTE: not called if isDiagram
+ removeListeners() {
+ let container = document.getElementById(this.containerId);
+ this.windowResizeObs.unobserve(container);
+ if ('onmousedown' in window) {
+ this.mouseListeners.forEach(ml => {
+ document.removeEventListener(ml.type, ml.listener);
+ });
+ }
+ if ('ontouchstart' in window) {
+ this.touchListeners.forEach(tl => {
+ // https://stackoverflow.com/a/42509310/12660887
+ document.removeEventListener(tl.type, tl.listener);
+ });
+ }
+ }
+
+ showChoices(moves, r) {
+ let container = document.getElementById(this.containerId);
+ let chessboard = container.querySelector(".chessboard");
+ let choices = document.createElement("div");
+ choices.id = "choices";
+ if (!r)
+ r = chessboard.getBoundingClientRect();
+ choices.style.width = r.width + "px";
+ choices.style.height = r.height + "px";
+ choices.style.left = r.x + "px";
+ choices.style.top = r.y + "px";
+ chessboard.style.opacity = "0.5";
+ container.appendChild(choices);
+ const squareWidth = r.width / this.size.y;
+ const firstUpLeft = (r.width - (moves.length * squareWidth)) / 2;
+ const firstUpTop = (r.height - squareWidth) / 2;
+ const color = moves[0].appear[0].c;
+ const callback = (m) => {
+ chessboard.style.opacity = "1";
+ container.removeChild(choices);
+ this.buildMoveStack(m, r);
+ }
+ for (let i=0; i < moves.length; i++) {
+ let choice = document.createElement("div");
+ choice.classList.add("choice");
+ choice.style.width = squareWidth + "px";
+ choice.style.height = squareWidth + "px";
+ choice.style.left = (firstUpLeft + i * squareWidth) + "px";
+ choice.style.top = firstUpTop + "px";
+ choice.style.backgroundColor = "lightyellow";
+ choice.onclick = () => callback(moves[i]);
+ const piece = document.createElement("piece");
+ const cdisp = moves[i].choice || moves[i].appear[0].p;
+ C.AddClass_es(piece,
+ this.pieces(color, moves[i].end.x, moves[i].end.y)[cdisp]["class"]);
+ piece.classList.add(V.GetColorClass(color));
+ piece.style.width = "100%";
+ piece.style.height = "100%";
+ choice.appendChild(piece);
+ choices.appendChild(choice);
+ }
+ }
+
+ displayMessage(elt, msg, classe_s, timeout) {
+ if (elt)
+ // Fixed element, e.g. for Dice Chess
+ elt.innerHTML = msg;
+ else {
+ // Temporary div (Chakart, Apocalypse...)
+ let divMsg = document.createElement("div");
+ C.AddClass_es(divMsg, classe_s);
+ divMsg.innerHTML = msg;
+ let container = document.getElementById(this.containerId);
+ container.appendChild(divMsg);
+ setTimeout(() => container.removeChild(divMsg), timeout);
+ }
+ }
+
+ ////////////////
+ // DARK METHODS
+
+ updateEnlightened() {
+ this.oldEnlightened = this.enlightened;
+ this.enlightened = ArrayFun.init(this.size.x, this.size.y, false);
+ // Add pieces positions + all squares reachable by moves (includes Zen):
+ for (let x=0; x<this.size.x; x++) {
+ for (let y=0; y<this.size.y; y++) {
+ if (this.board[x][y] != "" && this.getColor(x, y) == this.playerColor)
+ {
+ this.enlightened[x][y] = true;
+ this.getPotentialMovesFrom([x, y]).forEach(m => {
+ this.enlightened[m.end.x][m.end.y] = true;
+ });
+ }
+ }
+ }
+ if (this.epSquare)
+ this.enlightEnpassant();
+ }
+
+ // Include square of the en-passant capturing square:
+ enlightEnpassant() {
+ // NOTE: shortcut, pawn has only one attack type, doesn't depend on square
+ // TODO: (0, 0) is wrong, would need to place an attacker here...
+ const steps = this.pieces(this.playerColor, 0, 0)["p"].attack[0].steps;
+ for (let step of steps) {
+ const x = this.epSquare.x - step[0], //NOTE: epSquare.x not on edge
+ y = this.getY(this.epSquare.y - step[1]);
+ if (
+ this.onBoard(x, y) &&
+ this.getColor(x, y) == this.playerColor &&
+ this.getPieceType(x, y) == "p"
+ ) {
+ this.enlightened[x][this.epSquare.y] = true;
+ break;
+ }
+ }
+ }
+
+ // Apply diff this.enlightened --> oldEnlightened on board
+ graphUpdateEnlightened() {
+ let chessboard =
+ document.getElementById(this.containerId).querySelector(".chessboard");
+ const r = chessboard.getBoundingClientRect();
+ const pieceWidth = this.getPieceWidth(r.width);
+ for (let x=0; x<this.size.x; x++) {
+ for (let y=0; y<this.size.y; y++) {
+ if (!this.enlightened[x][y] && this.oldEnlightened[x][y]) {
+ let elt = document.getElementById(this.coordsToId({x: x, y: y}));
+ elt.classList.add("in-shadow");
+ if (this.g_pieces[x][y])
+ this.g_pieces[x][y].classList.add("hidden");
+ }
+ else if (this.enlightened[x][y] && !this.oldEnlightened[x][y]) {
+ let elt = document.getElementById(this.coordsToId({x: x, y: y}));
+ elt.classList.remove("in-shadow");
+ if (this.g_pieces[x][y])
+ this.g_pieces[x][y].classList.remove("hidden");
+ }
+ }
+ }
+ }
+
+ //////////////
+ // BASIC UTILS
+
+ get size() {