From dac395887d96e2d642b209c6db6aaacc3ffacb34 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Tue, 4 Feb 2020 23:38:53 +0100 Subject: [PATCH] Convert all remaining tabs by 2spaces --- TODO | 1 - client/public/index.html | 10 +- client/src/components/ChallengeList.vue | 2 +- client/src/components/ContactForm.vue | 4 +- client/src/components/GameList.vue | 4 +- client/src/components/Language.vue | 2 +- client/src/components/MoveList.vue | 126 +-- client/src/components/Settings.vue | 4 +- client/src/components/UpsertUser.vue | 28 +- client/src/data/challengeCheck.js | 18 +- client/src/data/userCheck.js | 28 +- client/src/parameters.js.dist | 8 +- client/src/router.js | 6 +- client/src/utils/ajax.js | 78 +- client/src/utils/datetime.js | 8 +- client/src/utils/printDiagram.js | 184 ++-- client/src/utils/squareId.js | 8 +- client/src/variants/Alice.js | 644 ++++++------ client/src/variants/Antiking.js | 392 ++++---- client/src/variants/Atomic.js | 260 ++--- client/src/variants/Baroque.js | 1226 +++++++++++------------ client/src/variants/Berolina.js | 252 ++--- client/src/variants/Checkered.js | 582 +++++------ client/src/variants/Chess960.js | 2 +- client/src/variants/Crazyhouse.js | 520 +++++----- client/src/variants/Dark.js | 524 +++++----- client/src/variants/Extinction.js | 230 ++--- client/src/variants/Grand.js | 768 +++++++------- client/src/variants/Losers.js | 356 +++---- client/src/variants/Magnetic.js | 388 +++---- client/src/variants/Marseille.js | 550 +++++----- client/src/variants/Upsidedown.js | 114 +-- client/src/variants/Wildebeest.js | 508 +++++----- client/src/variants/Zen.js | 448 ++++----- client/src/views/Hall.vue | 2 +- client/src/views/MyGames.vue | 2 +- server/app.js | 36 +- server/bin/www | 8 +- server/config/parameters.js.dist | 54 +- server/db/populate.sql | 36 +- server/gulpfile.js | 18 +- server/models/Game.js | 188 ++-- server/models/User.js | 186 ++-- server/models/Variant.js | 20 +- server/routes/games.js | 42 +- server/routes/messages.js | 22 +- server/routes/users.js | 146 +-- server/routes/variants.js | 10 +- server/utils/access.js | 114 +-- server/utils/database.js | 2 +- server/utils/mailer.js | 60 +- server/utils/tokenGenerator.js | 12 +- 52 files changed, 4620 insertions(+), 4621 deletions(-) diff --git a/TODO b/TODO index 53eab354..d8f28b4e 100644 --- a/TODO +++ b/TODO @@ -1,2 +1 @@ -MarseilleChess: revise bot and test Translations (including About page), refresh rules diff --git a/client/public/index.html b/client/public/index.html index 0ab70d5d..12eb803b 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -2,14 +2,14 @@ <html> <head> <meta charset="utf-8"> - <title>vchess - club</title> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <title>vchess - club</title> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> - <link rel="stylesheet" + <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css"> - <link rel="stylesheet" - href="//fonts.googleapis.com/css?family=Open+Sans:400,700"> + <link rel="stylesheet" + href="//fonts.googleapis.com/css?family=Open+Sans:400,700"> <style> body { --fore-color: #2c3e50; diff --git a/client/src/components/ChallengeList.vue b/client/src/components/ChallengeList.vue index 890ec1d9..c57073c0 100644 --- a/client/src/components/ChallengeList.vue +++ b/client/src/components/ChallengeList.vue @@ -20,7 +20,7 @@ import { store } from "@/store"; export default { name: "my-challenge-list", - props: ["challenges"], + props: ["challenges"], data: function() { return { st: store.state, diff --git a/client/src/components/ContactForm.vue b/client/src/components/ContactForm.vue index 734d7f19..45b7c011 100644 --- a/client/src/components/ContactForm.vue +++ b/client/src/components/ContactForm.vue @@ -34,7 +34,7 @@ export default { }; }, methods: { - // Note: not using Vue here, but would be possible + // Note: not using Vue here, but would be possible trySendMessage: function() { let email = document.getElementById("userEmail"); let subject = document.getElementById("mailSubject"); @@ -65,7 +65,7 @@ export default { } ); }, - }, + }, }; </script> diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue index 5ab61cc3..0d2fbf46 100644 --- a/client/src/components/GameList.vue +++ b/client/src/components/GameList.vue @@ -23,14 +23,14 @@ import { store } from "@/store"; export default { name: "my-game-list", - props: ["games"], + props: ["games"], data: function() { return { st: store.state, showResult: false, }; }, - computed: { + computed: { sortedGames: function() { // Show in order: games where it's my turn, my running games, my games, other games this.showResult = this.games.some(g => g.score != "*"); diff --git a/client/src/components/Language.vue b/client/src/components/Language.vue index c701e590..d35d8e21 100644 --- a/client/src/components/Language.vue +++ b/client/src/components/Language.vue @@ -40,6 +40,6 @@ export default { localStorage["lang"] = e.target.value; store.setLanguage(e.target.value); }, - }, + }, }; </script> diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue index e50047d9..90b76b43 100644 --- a/client/src/components/MoveList.vue +++ b/client/src/components/MoveList.vue @@ -2,7 +2,7 @@ // Component for moves list on the right export default { name: 'my-move-list', - props: ["moves","cursor","score","message","firstNum"], + props: ["moves","cursor","score","message","firstNum"], watch: { cursor: function(newCursor) { if (window.innerWidth <= 767) @@ -32,75 +32,75 @@ export default { }); }, }, - render(h) { - if (this.moves.length == 0) - return; - let tableContent = []; - let moveCounter = 0; - let tableRow = undefined; - let moveCells = undefined; - let curCellContent = ""; - let firstIndex = 0; + render(h) { + if (this.moves.length == 0) + return; + let tableContent = []; + let moveCounter = 0; + let tableRow = undefined; + let moveCells = undefined; + let curCellContent = ""; + let firstIndex = 0; for (let i=0; i<this.moves.length; i++) - { - if (this.moves[i].color == "w") - { - if (i == 0 || i>0 && this.moves[i-1].color=="b") - { - if (!!tableRow) - { - tableRow.children = moveCells; - tableContent.push(tableRow); - } - moveCells = [ - h( - "td", - { domProps: { innerHTML: (++moveCounter) + "." } } - ) - ]; - tableRow = h( - "tr", - { } - ); - curCellContent = ""; + { + if (this.moves[i].color == "w") + { + if (i == 0 || i>0 && this.moves[i-1].color=="b") + { + if (!!tableRow) + { + tableRow.children = moveCells; + tableContent.push(tableRow); + } + moveCells = [ + h( + "td", + { domProps: { innerHTML: (++moveCounter) + "." } } + ) + ]; + tableRow = h( + "tr", + { } + ); + curCellContent = ""; firstIndex = i; - } - } + } + } // Next condition is fine because even if the first move is black, // there will be the "..." which count as white move. else if (this.moves[i].color == "b" && this.moves[i-1].color == "w") firstIndex = i; - curCellContent += this.moves[i].notation; - if (i < this.moves.length-1 && this.moves[i+1].color == this.moves[i].color) - curCellContent += ","; - else //color change - { - moveCells.push( - h( - "td", - { - domProps: { innerHTML: curCellContent }, - on: { click: () => this.gotoMove(i) }, + curCellContent += this.moves[i].notation; + if (i < this.moves.length-1 && this.moves[i+1].color == this.moves[i].color) + curCellContent += ","; + else //color change + { + moveCells.push( + h( + "td", + { + domProps: { innerHTML: curCellContent }, + on: { click: () => this.gotoMove(i) }, "class": { "highlight-lm": this.cursor >= firstIndex && this.cursor <= i }, - } - ) - ); - curCellContent = ""; - } - } - // Complete last row, which might not be full: - if (moveCells.length-1 == 1) - { + } + ) + ); + curCellContent = ""; + } + } + // Complete last row, which might not be full: + if (moveCells.length-1 == 1) + { moveCells.push( h( "td", { domProps: { innerHTML: "" } } ) ); - } - tableRow.children = moveCells; - tableContent.push(tableRow); + } + tableRow.children = moveCells; + tableContent.push(tableRow); let rootElements = []; if (this.score != "*") { @@ -127,18 +127,18 @@ export default { }, }, tableContent - ) + ) ); - return h( + return h( "div", { }, rootElements); - }, + }, methods: { - gotoMove: function(index) { - this.$emit("goto-move", index); - }, - }, + gotoMove: function(index) { + this.$emit("goto-move", index); + }, + }, }; </script> diff --git a/client/src/components/Settings.vue b/client/src/components/Settings.vue index 92b463ef..1cfbdc8f 100644 --- a/client/src/components/Settings.vue +++ b/client/src/components/Settings.vue @@ -67,7 +67,7 @@ export default { } }); }, - methods: { + methods: { updateSettings: function(event) { const propName = event.target.id.substr(3).replace(/^\w/, c => c.toLowerCase()) @@ -90,6 +90,6 @@ export default { document.getElementById("gameContainer").style.width = (boardSize + movesWidth) + "px"; }, - }, + }, }; </script> diff --git a/client/src/components/UpsertUser.vue b/client/src/components/UpsertUser.vue index 51edba33..ecd51fa9 100644 --- a/client/src/components/UpsertUser.vue +++ b/client/src/components/UpsertUser.vue @@ -46,20 +46,20 @@ export default { enterTime: Number.MAX_SAFE_INTEGER, //for a basic anti-bot strategy }; }, - watch: { - nameOrEmail: function(newValue) { - if (newValue.indexOf('@') >= 0) - { - this.user.email = newValue; - this.user.name = ""; - } - else - { - this.user.name = newValue; - this.user.email = ""; - } - }, - }, + watch: { + nameOrEmail: function(newValue) { + if (newValue.indexOf('@') >= 0) + { + this.user.email = newValue; + this.user.name = ""; + } + else + { + this.user.name = newValue; + this.user.email = ""; + } + }, + }, computed: { submitMessage: function() { switch (this.stage) diff --git a/client/src/data/challengeCheck.js b/client/src/data/challengeCheck.js index c2700008..1a8b9fee 100644 --- a/client/src/data/challengeCheck.js +++ b/client/src/data/challengeCheck.js @@ -2,21 +2,21 @@ import { extractTime } from "@/utils/timeControl"; export function checkChallenge(c) { - const vid = parseInt(c.vid); - if (isNaN(vid) || vid <= 0) - return "Please select a variant"; + const vid = parseInt(c.vid); + if (isNaN(vid) || vid <= 0) + return "Please select a variant"; const tc = extractTime(c.timeControl); if (!tc) return "Wrong time control"; - // Basic alphanumeric check for opponent name - if (!!c.to) - { + // Basic alphanumeric check for opponent name + if (!!c.to) + { // TODO: slightly redundant (see data/userCheck.js) - if (!c.to.match(/^[\w]+$/)) - return "Wrong characters in opponent name"; - } + if (!c.to.match(/^[\w]+$/)) + return "Wrong characters in opponent name"; + } // Allow custom FEN (and check it) only for individual challenges if (c.fen.length > 0 && !!c.to) diff --git a/client/src/data/userCheck.js b/client/src/data/userCheck.js index 4c714a68..9eb85622 100644 --- a/client/src/data/userCheck.js +++ b/client/src/data/userCheck.js @@ -1,17 +1,17 @@ export function checkNameEmail(o) { - if (typeof o.name === "string") - { - if (o.name.length == 0) - return "Empty name"; - if (!o.name.match(/^[\w]+$/)) - return "Bad characters in name"; - } - if (typeof o.email === "string") - { - if (o.email.length == 0) - return "Empty email"; - if (!o.email.match(/^[\w.+-]+@[\w.+-]+$/)) - return "Bad characters in email"; - } + if (typeof o.name === "string") + { + if (o.name.length == 0) + return "Empty name"; + if (!o.name.match(/^[\w]+$/)) + return "Bad characters in name"; + } + if (typeof o.email === "string") + { + if (o.email.length == 0) + return "Empty email"; + if (!o.email.match(/^[\w.+-]+@[\w.+-]+$/)) + return "Bad characters in email"; + } } diff --git a/client/src/parameters.js.dist b/client/src/parameters.js.dist index 5710bac0..edad3bbd 100644 --- a/client/src/parameters.js.dist +++ b/client/src/parameters.js.dist @@ -1,10 +1,10 @@ const Parameters = { - // URL of your socket server - socketUrl: "ws://localhost:3000", + // URL of your socket server + socketUrl: "ws://localhost:3000", - // URL of the server (leave blank for 1-server case) - serverUrl: "http://localhost:3000", + // URL of the server (leave blank for 1-server case) + serverUrl: "http://localhost:3000", // true if the server is at a different address cors: true, diff --git a/client/src/router.js b/client/src/router.js index 1eb88f91..599cfcb3 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -5,7 +5,7 @@ import Hall from "./views/Hall.vue"; Vue.use(Router); function loadView(view) { - return () => import(/* webpackChunkName: "view-[request]" */ `@/views/${view}.vue`) + return () => import(/* webpackChunkName: "view-[request]" */ `@/views/${view}.vue`) } import { ajax } from "@/utils/ajax"; @@ -46,8 +46,8 @@ const router = new Router({ localStorage["myname"] = res.name; localStorage["myid"] = res.id; } - // TODO: I don't like these 2 lines, "next('/')" should be enough - window.location = "/"; + // TODO: I don't like these 2 lines, "next('/')" should be enough + window.location = "/"; next(); } ); diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js index eb30330a..adfba467 100644 --- a/client/src/utils/ajax.js +++ b/client/src/utils/ajax.js @@ -7,64 +7,64 @@ import params from "../parameters"; //for server URL // From JSON (encoded string values!) to "arg1=...&arg2=..." function toQueryString(data) { - let data_str = ""; - Object.keys(data).forEach(k => { - data_str += k + "=" + encodeURIComponent(data[k]) + "&"; - }); - return data_str.slice(0, -1); //remove last "&" + let data_str = ""; + Object.keys(data).forEach(k => { + data_str += k + "=" + encodeURIComponent(data[k]) + "&"; + }); + return data_str.slice(0, -1); //remove last "&" } // data, error: optional export function ajax(url, method, data, success, error) { - let xhr = new XMLHttpRequest(); - if (data === undefined || typeof(data) === "function") //no data - { - error = success; - success = data; - data = {}; - } + let xhr = new XMLHttpRequest(); + if (data === undefined || typeof(data) === "function") //no data + { + error = success; + success = data; + data = {}; + } if (!success) success = () => {}; //by default, do nothing - if (!error) - error = errmsg => { alert(errmsg); }; - xhr.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) - { + if (!error) + error = errmsg => { alert(errmsg); }; + xhr.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) + { let res_json = ""; - try { - res_json = JSON.parse(xhr.responseText); + try { + res_json = JSON.parse(xhr.responseText); } catch (e) { - // Plain text (e.g. for rules retrieval) - return success(xhr.responseText); + // Plain text (e.g. for rules retrieval) + return success(xhr.responseText); } if (!res_json.errmsg && !res_json.errno) success(res_json); - else + else { if (!!res_json.errmsg) - error(res_json.errmsg); + error(res_json.errmsg); else error(res_json.code + ". errno = " + res_json.errno); } - } - }; + } + }; - if (["GET","DELETE"].includes(method) && !!data) - { - // Append query params to URL - url += "/?" + toQueryString(data); - } - xhr.open(method, params.serverUrl + url, true); - xhr.setRequestHeader('X-Requested-With', "XMLHttpRequest"); - // Next line to allow cross-domain cookies in dev mode (TODO: if...) + if (["GET","DELETE"].includes(method) && !!data) + { + // Append query params to URL + url += "/?" + toQueryString(data); + } + xhr.open(method, params.serverUrl + url, true); + xhr.setRequestHeader('X-Requested-With', "XMLHttpRequest"); + // Next line to allow cross-domain cookies in dev mode (TODO: if...) if (params.cors) xhr.withCredentials = true; if (["POST","PUT"].includes(method)) - { - xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); - xhr.send(JSON.stringify(data)); - } - else - xhr.send(); + { + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.send(JSON.stringify(data)); + } + else + xhr.send(); } diff --git a/client/src/utils/datetime.js b/client/src/utils/datetime.js index 49b70caf..5addd255 100644 --- a/client/src/utils/datetime.js +++ b/client/src/utils/datetime.js @@ -1,17 +1,17 @@ function zeroPad(x) { - return (x<10 ? "0" : "") + x; + return (x<10 ? "0" : "") + x; } export function getDate(d) { - return d.getFullYear() + '-' + zeroPad(d.getMonth()+1) + '-' + zeroPad(d.getDate()); + return d.getFullYear() + '-' + zeroPad(d.getMonth()+1) + '-' + zeroPad(d.getDate()); } export function getTime(d) { - return zeroPad(d.getHours()) + ":" + zeroPad(d.getMinutes()) + ":" + - zeroPad(d.getSeconds()); + return zeroPad(d.getHours()) + ":" + zeroPad(d.getMinutes()) + ":" + + zeroPad(d.getSeconds()); } function padDigits(x) diff --git a/client/src/utils/printDiagram.js b/client/src/utils/printDiagram.js index 0d8d9ae3..bf078cee 100644 --- a/client/src/utils/printDiagram.js +++ b/client/src/utils/printDiagram.js @@ -3,109 +3,109 @@ import { ArrayFun } from "@/utils/array"; // Turn (human) marks into coordinates function getMarkArray(marks) { - if (!marks || marks == "-") - return []; - let markArray = ArrayFun.init(V.size.x, V.size.y, false); - const squares = marks.split(","); - for (let i=0; i<squares.length; i++) - { - const coords = V.SquareToCoords(squares[i]); - markArray[coords.x][coords.y] = true; - } - return markArray; + if (!marks || marks == "-") + return []; + let markArray = ArrayFun.init(V.size.x, V.size.y, false); + const squares = marks.split(","); + for (let i=0; i<squares.length; i++) + { + const coords = V.SquareToCoords(squares[i]); + markArray[coords.x][coords.y] = true; + } + return markArray; } // Turn (human) shadow indications into coordinates function getShadowArray(shadow) { - if (!shadow || shadow == "-") - return []; - let shadowArray = ArrayFun.init(V.size.x, V.size.y, false); - const squares = shadow.split(","); - for (let i=0; i<squares.length; i++) - { - const rownum = V.size.x - parseInt(squares[i]); - if (!isNaN(rownum)) - { - // Shadow a full row - for (let i=0; i<V.size.y; i++) - shadowArray[rownum][i] = true; - continue; - } - if (squares[i].length == 1) - { - // Shadow a full column - const colnum = V.ColumnToCoord(squares[i]); - for (let i=0; i<V.size.x; i++) - shadowArray[i][colnum] = true; - continue; - } - if (squares[i].indexOf("-") >= 0) - { - // Shadow a range of squares, horizontally or vertically - const firstLastSq = squares[i].split("-"); - const range = - [ - V.SquareToCoords(firstLastSq[0]), - V.SquareToCoords(firstLastSq[1]) - ]; - const step = - [ - range[1].x == range[0].x - ? 0 - : (range[1].x - range[0].x) / Math.abs(range[1].x - range[0].x), - range[1].y == range[0].y - ? 0 - : (range[1].y - range[0].y) / Math.abs(range[1].y - range[0].y) - ]; - // Convention: range always from smaller to larger number - for (let x=range[0].x, y=range[0].y; x <= range[1].x && y <= range[1].y; - x += step[0], y += step[1]) - { - shadowArray[x][y] = true; - } - continue; - } - // Shadow just one square: - const coords = V.SquareToCoords(squares[i]); - shadowArray[coords.x][coords.y] = true; - } - return shadowArray; + if (!shadow || shadow == "-") + return []; + let shadowArray = ArrayFun.init(V.size.x, V.size.y, false); + const squares = shadow.split(","); + for (let i=0; i<squares.length; i++) + { + const rownum = V.size.x - parseInt(squares[i]); + if (!isNaN(rownum)) + { + // Shadow a full row + for (let i=0; i<V.size.y; i++) + shadowArray[rownum][i] = true; + continue; + } + if (squares[i].length == 1) + { + // Shadow a full column + const colnum = V.ColumnToCoord(squares[i]); + for (let i=0; i<V.size.x; i++) + shadowArray[i][colnum] = true; + continue; + } + if (squares[i].indexOf("-") >= 0) + { + // Shadow a range of squares, horizontally or vertically + const firstLastSq = squares[i].split("-"); + const range = + [ + V.SquareToCoords(firstLastSq[0]), + V.SquareToCoords(firstLastSq[1]) + ]; + const step = + [ + range[1].x == range[0].x + ? 0 + : (range[1].x - range[0].x) / Math.abs(range[1].x - range[0].x), + range[1].y == range[0].y + ? 0 + : (range[1].y - range[0].y) / Math.abs(range[1].y - range[0].y) + ]; + // Convention: range always from smaller to larger number + for (let x=range[0].x, y=range[0].y; x <= range[1].x && y <= range[1].y; + x += step[0], y += step[1]) + { + shadowArray[x][y] = true; + } + continue; + } + // Shadow just one square: + const coords = V.SquareToCoords(squares[i]); + shadowArray[coords.x][coords.y] = true; + } + return shadowArray; } // args: object with position (mandatory), and // orientation, marks, shadow (optional) export function getDiagram(args) { - // Obtain the array of pieces images names: - const board = V.GetBoard(args.position); - const orientation = args.orientation || "w"; - const markArray = getMarkArray(args.marks); - const shadowArray = getShadowArray(args.shadow); - let boardDiv = ""; - const [startX,startY,inc] = orientation == 'w' - ? [0, 0, 1] - : [V.size.x-1, V.size.y-1, -1]; - for (let i=startX; i>=0 && i<V.size.x; i+=inc) - { - boardDiv += "<div class='row'>"; - for (let j=startY; j>=0 && j<V.size.y; j+=inc) - { - boardDiv += "<div class='board board" + V.size.y + " " + - ((i+j)%2==0 ? "light-square-diag" : "dark-square-diag") + - (shadowArray.length > 0 && shadowArray[i][j] ? " in-shadow" : "") + - "'>"; - if (board[i][j] != V.EMPTY) - { - boardDiv += "<img " + + // Obtain the array of pieces images names: + const board = V.GetBoard(args.position); + const orientation = args.orientation || "w"; + const markArray = getMarkArray(args.marks); + const shadowArray = getShadowArray(args.shadow); + let boardDiv = ""; + const [startX,startY,inc] = orientation == 'w' + ? [0, 0, 1] + : [V.size.x-1, V.size.y-1, -1]; + for (let i=startX; i>=0 && i<V.size.x; i+=inc) + { + boardDiv += "<div class='row'>"; + for (let j=startY; j>=0 && j<V.size.y; j+=inc) + { + boardDiv += "<div class='board board" + V.size.y + " " + + ((i+j)%2==0 ? "light-square-diag" : "dark-square-diag") + + (shadowArray.length > 0 && shadowArray[i][j] ? " in-shadow" : "") + + "'>"; + if (board[i][j] != V.EMPTY) + { + boardDiv += "<img " + "src='/images/pieces/" + V.getPpath(board[i][j]) + ".svg' " + "class='piece'/>"; - } - if (markArray.length > 0 && markArray[i][j]) - boardDiv += "<img src='/images/mark.svg' class='mark-square'/>"; - boardDiv += "</div>"; - } - boardDiv += "</div>"; - } - return boardDiv; + } + if (markArray.length > 0 && markArray[i][j]) + boardDiv += "<img src='/images/mark.svg' class='mark-square'/>"; + boardDiv += "</div>"; + } + boardDiv += "</div>"; + } + return boardDiv; } diff --git a/client/src/utils/squareId.js b/client/src/utils/squareId.js index a68b51f9..6469bc3f 100644 --- a/client/src/utils/squareId.js +++ b/client/src/utils/squareId.js @@ -1,13 +1,13 @@ // Get the identifier of a HTML square from its numeric coordinates o.x,o.y. export function getSquareId(o) { - // NOTE: a separator is required to allow any size of board - return "sq-" + o.x + "-" + o.y; + // NOTE: a separator is required to allow any size of board + return "sq-" + o.x + "-" + o.y; } // Inverse function export function getSquareFromId(id) { - const idParts = id.split('-'); - return [parseInt(idParts[1]), parseInt(idParts[2])]; + const idParts = id.split('-'); + return [parseInt(idParts[1]), parseInt(idParts[2])]; } diff --git a/client/src/variants/Alice.js b/client/src/variants/Alice.js index e81d3fc8..d7e775cf 100644 --- a/client/src/variants/Alice.js +++ b/client/src/variants/Alice.js @@ -5,355 +5,355 @@ import { ArrayFun} from "@/utils/array"; // TODO? atLeastOneMove() would be more efficient if rewritten here (less sideBoard computations) export const VariantRules = class AliceRules extends ChessRules { - static get ALICE_PIECES() - { - return { - 's': 'p', - 't': 'q', - 'u': 'r', - 'c': 'b', - 'o': 'n', - 'l': 'k', - }; - } - static get ALICE_CODES() - { - return { - 'p': 's', - 'q': 't', - 'r': 'u', - 'b': 'c', - 'n': 'o', - 'k': 'l', - }; - } + static get ALICE_PIECES() + { + return { + 's': 'p', + 't': 'q', + 'u': 'r', + 'c': 'b', + 'o': 'n', + 'l': 'k', + }; + } + static get ALICE_CODES() + { + return { + 'p': 's', + 'q': 't', + 'r': 'u', + 'b': 'c', + 'n': 'o', + 'k': 'l', + }; + } - static getPpath(b) - { - return (Object.keys(this.ALICE_PIECES).includes(b[1]) ? "Alice/" : "") + b; - } + static getPpath(b) + { + return (Object.keys(this.ALICE_PIECES).includes(b[1]) ? "Alice/" : "") + b; + } - static get PIECES() - { - return ChessRules.PIECES.concat(Object.keys(V.ALICE_PIECES)); - } + static get PIECES() + { + return ChessRules.PIECES.concat(Object.keys(V.ALICE_PIECES)); + } - setOtherVariables(fen) - { - super.setOtherVariables(fen); - const rows = V.ParseFen(fen).position.split("/"); - if (this.kingPos["w"][0] < 0 || this.kingPos["b"][0] < 0) - { - // INIT_COL_XXX won't be required if Alice kings are found (means 'king moved') - for (let i=0; i<rows.length; i++) - { - let k = 0; //column index on board - for (let j=0; j<rows[i].length; j++) - { - switch (rows[i].charAt(j)) - { - case 'l': - this.kingPos['b'] = [i,k]; - break; - case 'L': - this.kingPos['w'] = [i,k]; - break; - default: - const num = parseInt(rows[i].charAt(j)); - if (!isNaN(num)) - k += (num-1); - } - k++; - } - } - } - } + setOtherVariables(fen) + { + super.setOtherVariables(fen); + const rows = V.ParseFen(fen).position.split("/"); + if (this.kingPos["w"][0] < 0 || this.kingPos["b"][0] < 0) + { + // INIT_COL_XXX won't be required if Alice kings are found (means 'king moved') + for (let i=0; i<rows.length; i++) + { + let k = 0; //column index on board + for (let j=0; j<rows[i].length; j++) + { + switch (rows[i].charAt(j)) + { + case 'l': + this.kingPos['b'] = [i,k]; + break; + case 'L': + this.kingPos['w'] = [i,k]; + break; + default: + const num = parseInt(rows[i].charAt(j)); + if (!isNaN(num)) + k += (num-1); + } + k++; + } + } + } + } - // Return the (standard) color+piece notation at a square for a board - getSquareOccupation(i, j, mirrorSide) - { - const piece = this.getPiece(i,j); - if (mirrorSide==1 && Object.keys(V.ALICE_CODES).includes(piece)) - return this.board[i][j]; - else if (mirrorSide==2 && Object.keys(V.ALICE_PIECES).includes(piece)) - return this.getColor(i,j) + V.ALICE_PIECES[piece]; - return ""; - } + // Return the (standard) color+piece notation at a square for a board + getSquareOccupation(i, j, mirrorSide) + { + const piece = this.getPiece(i,j); + if (mirrorSide==1 && Object.keys(V.ALICE_CODES).includes(piece)) + return this.board[i][j]; + else if (mirrorSide==2 && Object.keys(V.ALICE_PIECES).includes(piece)) + return this.getColor(i,j) + V.ALICE_PIECES[piece]; + return ""; + } - // Build board of the given (mirror)side - getSideBoard(mirrorSide) - { - // Build corresponding board from complete board - let sideBoard = ArrayFun.init(V.size.x, V.size.y, ""); - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - sideBoard[i][j] = this.getSquareOccupation(i, j, mirrorSide); - } - return sideBoard; - } + // Build board of the given (mirror)side + getSideBoard(mirrorSide) + { + // Build corresponding board from complete board + let sideBoard = ArrayFun.init(V.size.x, V.size.y, ""); + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + sideBoard[i][j] = this.getSquareOccupation(i, j, mirrorSide); + } + return sideBoard; + } - // NOTE: castle & enPassant https://www.chessvariants.com/other.dir/alice.html - getPotentialMovesFrom([x,y], sideBoard) - { - const pieces = Object.keys(V.ALICE_CODES); - const codes = Object.keys(V.ALICE_PIECES); - const mirrorSide = (pieces.includes(this.getPiece(x,y)) ? 1 : 2); + // NOTE: castle & enPassant https://www.chessvariants.com/other.dir/alice.html + getPotentialMovesFrom([x,y], sideBoard) + { + const pieces = Object.keys(V.ALICE_CODES); + const codes = Object.keys(V.ALICE_PIECES); + const mirrorSide = (pieces.includes(this.getPiece(x,y)) ? 1 : 2); if (!sideBoard) - sideBoard = [this.getSideBoard(1), this.getSideBoard(2)]; - const color = this.getColor(x,y); + sideBoard = [this.getSideBoard(1), this.getSideBoard(2)]; + const color = this.getColor(x,y); - // Search valid moves on sideBoard - const saveBoard = this.board; - this.board = sideBoard[mirrorSide-1]; - const moves = super.getPotentialMovesFrom([x,y]) - .filter(m => { - // Filter out king moves which result in under-check position on - // current board (before mirror traversing) - let aprioriValid = true; - if (m.appear[0].p == V.KING) - { - this.play(m); - if (this.underCheck(color, sideBoard)) - aprioriValid = false; - this.undo(m); - } - return aprioriValid; - }); - this.board = saveBoard; + // Search valid moves on sideBoard + const saveBoard = this.board; + this.board = sideBoard[mirrorSide-1]; + const moves = super.getPotentialMovesFrom([x,y]) + .filter(m => { + // Filter out king moves which result in under-check position on + // current board (before mirror traversing) + let aprioriValid = true; + if (m.appear[0].p == V.KING) + { + this.play(m); + if (this.underCheck(color, sideBoard)) + aprioriValid = false; + this.undo(m); + } + return aprioriValid; + }); + this.board = saveBoard; - // Finally filter impossible moves - const res = moves.filter(m => { - if (m.appear.length == 2) //castle - { - // appear[i] must be an empty square on the other board - for (let psq of m.appear) - { - if (this.getSquareOccupation(psq.x,psq.y,3-mirrorSide) != V.EMPTY) - return false; - } - } - else if (this.board[m.end.x][m.end.y] != V.EMPTY) - { - // Attempt to capture - const piece = this.getPiece(m.end.x,m.end.y); - if ((mirrorSide==1 && codes.includes(piece)) - || (mirrorSide==2 && pieces.includes(piece))) - { - return false; - } - } - // If the move is computed on board1, m.appear change for Alice pieces. - if (mirrorSide==1) - { - m.appear.forEach(psq => { //forEach: castling taken into account - psq.p = V.ALICE_CODES[psq.p]; //goto board2 - }); - } - else //move on board2: mark vanishing pieces as Alice - { - m.vanish.forEach(psq => { - psq.p = V.ALICE_CODES[psq.p]; - }); - } - // Fix en-passant captures - if (m.vanish[0].p == V.PAWN && m.vanish.length == 2 - && this.board[m.end.x][m.end.y] == V.EMPTY) - { - m.vanish[1].c = V.GetOppCol(this.getColor(x,y)); - // In the special case of en-passant, if - // - board1 takes board2 : vanish[1] --> Alice - // - board2 takes board1 : vanish[1] --> normal - let van = m.vanish[1]; - if (mirrorSide==1 && codes.includes(this.getPiece(van.x,van.y))) - van.p = V.ALICE_CODES[van.p]; - else if (mirrorSide==2 && pieces.includes(this.getPiece(van.x,van.y))) - van.p = V.ALICE_PIECES[van.p]; - } - return true; - }); - return res; - } + // Finally filter impossible moves + const res = moves.filter(m => { + if (m.appear.length == 2) //castle + { + // appear[i] must be an empty square on the other board + for (let psq of m.appear) + { + if (this.getSquareOccupation(psq.x,psq.y,3-mirrorSide) != V.EMPTY) + return false; + } + } + else if (this.board[m.end.x][m.end.y] != V.EMPTY) + { + // Attempt to capture + const piece = this.getPiece(m.end.x,m.end.y); + if ((mirrorSide==1 && codes.includes(piece)) + || (mirrorSide==2 && pieces.includes(piece))) + { + return false; + } + } + // If the move is computed on board1, m.appear change for Alice pieces. + if (mirrorSide==1) + { + m.appear.forEach(psq => { //forEach: castling taken into account + psq.p = V.ALICE_CODES[psq.p]; //goto board2 + }); + } + else //move on board2: mark vanishing pieces as Alice + { + m.vanish.forEach(psq => { + psq.p = V.ALICE_CODES[psq.p]; + }); + } + // Fix en-passant captures + if (m.vanish[0].p == V.PAWN && m.vanish.length == 2 + && this.board[m.end.x][m.end.y] == V.EMPTY) + { + m.vanish[1].c = V.GetOppCol(this.getColor(x,y)); + // In the special case of en-passant, if + // - board1 takes board2 : vanish[1] --> Alice + // - board2 takes board1 : vanish[1] --> normal + let van = m.vanish[1]; + if (mirrorSide==1 && codes.includes(this.getPiece(van.x,van.y))) + van.p = V.ALICE_CODES[van.p]; + else if (mirrorSide==2 && pieces.includes(this.getPiece(van.x,van.y))) + van.p = V.ALICE_PIECES[van.p]; + } + return true; + }); + return res; + } - filterValid(moves, sideBoard) - { - if (moves.length == 0) - return []; - if (!sideBoard) + filterValid(moves, sideBoard) + { + if (moves.length == 0) + return []; + if (!sideBoard) sideBoard = [this.getSideBoard(1), this.getSideBoard(2)]; - const color = this.turn; - return moves.filter(m => { - this.playSide(m, sideBoard); //no need to track flags - const res = !this.underCheck(color, sideBoard); - this.undoSide(m, sideBoard); - return res; - }); - } + const color = this.turn; + return moves.filter(m => { + this.playSide(m, sideBoard); //no need to track flags + const res = !this.underCheck(color, sideBoard); + this.undoSide(m, sideBoard); + return res; + }); + } - getAllValidMoves() - { - const color = this.turn; - const oppCol = V.GetOppCol(color); - let potentialMoves = []; - const sideBoard = [this.getSideBoard(1), this.getSideBoard(2)]; - for (var i=0; i<V.size.x; i++) - { - for (var j=0; j<V.size.y; j++) - { - if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == color) - { - Array.prototype.push.apply(potentialMoves, - this.getPotentialMovesFrom([i,j], sideBoard)); - } - } - } - return this.filterValid(potentialMoves, sideBoard); - } + getAllValidMoves() + { + const color = this.turn; + const oppCol = V.GetOppCol(color); + let potentialMoves = []; + const sideBoard = [this.getSideBoard(1), this.getSideBoard(2)]; + for (var i=0; i<V.size.x; i++) + { + for (var j=0; j<V.size.y; j++) + { + if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == color) + { + Array.prototype.push.apply(potentialMoves, + this.getPotentialMovesFrom([i,j], sideBoard)); + } + } + } + return this.filterValid(potentialMoves, sideBoard); + } - // Play on sideboards [TODO: only one sideBoard required] - playSide(move, sideBoard) - { - const pieces = Object.keys(V.ALICE_CODES); - move.vanish.forEach(psq => { - const mirrorSide = (pieces.includes(psq.p) ? 1 : 2); - sideBoard[mirrorSide-1][psq.x][psq.y] = V.EMPTY; - }); - move.appear.forEach(psq => { - const mirrorSide = (pieces.includes(psq.p) ? 1 : 2); - const piece = (mirrorSide == 1 ? psq.p : V.ALICE_PIECES[psq.p]); - sideBoard[mirrorSide-1][psq.x][psq.y] = psq.c + piece; - if (piece == V.KING) - this.kingPos[psq.c] = [psq.x,psq.y]; - }); - } + // Play on sideboards [TODO: only one sideBoard required] + playSide(move, sideBoard) + { + const pieces = Object.keys(V.ALICE_CODES); + move.vanish.forEach(psq => { + const mirrorSide = (pieces.includes(psq.p) ? 1 : 2); + sideBoard[mirrorSide-1][psq.x][psq.y] = V.EMPTY; + }); + move.appear.forEach(psq => { + const mirrorSide = (pieces.includes(psq.p) ? 1 : 2); + const piece = (mirrorSide == 1 ? psq.p : V.ALICE_PIECES[psq.p]); + sideBoard[mirrorSide-1][psq.x][psq.y] = psq.c + piece; + if (piece == V.KING) + this.kingPos[psq.c] = [psq.x,psq.y]; + }); + } - // Undo on sideboards - undoSide(move, sideBoard) - { - const pieces = Object.keys(V.ALICE_CODES); - move.appear.forEach(psq => { - const mirrorSide = (pieces.includes(psq.p) ? 1 : 2); - sideBoard[mirrorSide-1][psq.x][psq.y] = V.EMPTY; - }); - move.vanish.forEach(psq => { - const mirrorSide = (pieces.includes(psq.p) ? 1 : 2); - const piece = (mirrorSide == 1 ? psq.p : V.ALICE_PIECES[psq.p]); - sideBoard[mirrorSide-1][psq.x][psq.y] = psq.c + piece; - if (piece == V.KING) - this.kingPos[psq.c] = [psq.x,psq.y]; - }); - } + // Undo on sideboards + undoSide(move, sideBoard) + { + const pieces = Object.keys(V.ALICE_CODES); + move.appear.forEach(psq => { + const mirrorSide = (pieces.includes(psq.p) ? 1 : 2); + sideBoard[mirrorSide-1][psq.x][psq.y] = V.EMPTY; + }); + move.vanish.forEach(psq => { + const mirrorSide = (pieces.includes(psq.p) ? 1 : 2); + const piece = (mirrorSide == 1 ? psq.p : V.ALICE_PIECES[psq.p]); + sideBoard[mirrorSide-1][psq.x][psq.y] = psq.c + piece; + if (piece == V.KING) + this.kingPos[psq.c] = [psq.x,psq.y]; + }); + } // sideBoard: arg containing both boards (see getAllValidMoves()) - underCheck(color, sideBoard) - { - const kp = this.kingPos[color]; - const mirrorSide = (sideBoard[0][kp[0]][kp[1]] != V.EMPTY ? 1 : 2); - let saveBoard = this.board; - this.board = sideBoard[mirrorSide-1]; - let res = this.isAttacked(kp, [V.GetOppCol(color)]); - this.board = saveBoard; - return res; - } + underCheck(color, sideBoard) + { + const kp = this.kingPos[color]; + const mirrorSide = (sideBoard[0][kp[0]][kp[1]] != V.EMPTY ? 1 : 2); + let saveBoard = this.board; + this.board = sideBoard[mirrorSide-1]; + let res = this.isAttacked(kp, [V.GetOppCol(color)]); + this.board = saveBoard; + return res; + } - getCheckSquares(color) - { - const pieces = Object.keys(V.ALICE_CODES); - const kp = this.kingPos[color]; - const mirrorSide = (pieces.includes(this.getPiece(kp[0],kp[1])) ? 1 : 2); - let sideBoard = this.getSideBoard(mirrorSide); - let saveBoard = this.board; - this.board = sideBoard; - let res = this.isAttacked(this.kingPos[color], [V.GetOppCol(color)]) - ? [ JSON.parse(JSON.stringify(this.kingPos[color])) ] - : [ ]; - this.board = saveBoard; - return res; - } + getCheckSquares(color) + { + const pieces = Object.keys(V.ALICE_CODES); + const kp = this.kingPos[color]; + const mirrorSide = (pieces.includes(this.getPiece(kp[0],kp[1])) ? 1 : 2); + let sideBoard = this.getSideBoard(mirrorSide); + let saveBoard = this.board; + this.board = sideBoard; + let res = this.isAttacked(this.kingPos[color], [V.GetOppCol(color)]) + ? [ JSON.parse(JSON.stringify(this.kingPos[color])) ] + : [ ]; + this.board = saveBoard; + return res; + } - updateVariables(move) - { - super.updateVariables(move); //standard king - const piece = move.vanish[0].p; - const c = move.vanish[0].c; - // "l" = Alice king - if (piece == "l") - { - this.kingPos[c][0] = move.appear[0].x; - this.kingPos[c][1] = move.appear[0].y; - this.castleFlags[c] = [false,false]; - } - } + updateVariables(move) + { + super.updateVariables(move); //standard king + const piece = move.vanish[0].p; + const c = move.vanish[0].c; + // "l" = Alice king + if (piece == "l") + { + this.kingPos[c][0] = move.appear[0].x; + this.kingPos[c][1] = move.appear[0].y; + this.castleFlags[c] = [false,false]; + } + } - unupdateVariables(move) - { - super.unupdateVariables(move); - const c = move.vanish[0].c; - if (move.vanish[0].p == "l") - this.kingPos[c] = [move.start.x, move.start.y]; - } + unupdateVariables(move) + { + super.unupdateVariables(move); + const c = move.vanish[0].c; + if (move.vanish[0].p == "l") + this.kingPos[c] = [move.start.x, move.start.y]; + } - getCurrentScore() - { + getCurrentScore() + { if (this.atLeastOneMove()) // game not over return "*"; const pieces = Object.keys(V.ALICE_CODES); - const color = this.turn; - const kp = this.kingPos[color]; - const mirrorSide = (pieces.includes(this.getPiece(kp[0],kp[1])) ? 1 : 2); - let sideBoard = this.getSideBoard(mirrorSide); - let saveBoard = this.board; - this.board = sideBoard; - let res = "*"; - if (!this.isAttacked(this.kingPos[color], [V.GetOppCol(color)])) - res = "1/2"; - else - res = (color == "w" ? "0-1" : "1-0"); - this.board = saveBoard; - return res; - } + const color = this.turn; + const kp = this.kingPos[color]; + const mirrorSide = (pieces.includes(this.getPiece(kp[0],kp[1])) ? 1 : 2); + let sideBoard = this.getSideBoard(mirrorSide); + let saveBoard = this.board; + this.board = sideBoard; + let res = "*"; + if (!this.isAttacked(this.kingPos[color], [V.GetOppCol(color)])) + res = "1/2"; + else + res = (color == "w" ? "0-1" : "1-0"); + this.board = saveBoard; + return res; + } - static get VALUES() - { - return Object.assign( - ChessRules.VALUES, - { - 's': 1, - 'u': 5, - 'o': 3, - 'c': 3, - 't': 9, - 'l': 1000, - } - ); - } + static get VALUES() + { + return Object.assign( + ChessRules.VALUES, + { + 's': 1, + 'u': 5, + 'o': 3, + 'c': 3, + 't': 9, + 'l': 1000, + } + ); + } - getNotation(move) - { - if (move.appear.length == 2 && move.appear[0].p == V.KING) - { - if (move.end.y < move.start.y) - return "0-0-0"; - else - return "0-0"; - } + getNotation(move) + { + if (move.appear.length == 2 && move.appear[0].p == V.KING) + { + if (move.end.y < move.start.y) + return "0-0-0"; + else + return "0-0"; + } - const finalSquare = V.CoordsToSquare(move.end); - const piece = this.getPiece(move.start.x, move.start.y); + const finalSquare = V.CoordsToSquare(move.end); + const piece = this.getPiece(move.start.x, move.start.y); - const captureMark = (move.vanish.length > move.appear.length ? "x" : ""); - let pawnMark = ""; - if (["p","s"].includes(piece) && captureMark.length == 1) - pawnMark = V.CoordToColumn(move.start.y); //start column + const captureMark = (move.vanish.length > move.appear.length ? "x" : ""); + let pawnMark = ""; + if (["p","s"].includes(piece) && captureMark.length == 1) + pawnMark = V.CoordToColumn(move.start.y); //start column - // Piece or pawn movement - let notation = piece.toUpperCase() + pawnMark + captureMark + finalSquare; - if (['s','p'].includes(piece) && !['s','p'].includes(move.appear[0].p)) - { - // Promotion - notation += "=" + move.appear[0].p.toUpperCase(); - } - return notation; - } + // Piece or pawn movement + let notation = piece.toUpperCase() + pawnMark + captureMark + finalSquare; + if (['s','p'].includes(piece) && !['s','p'].includes(move.appear[0].p)) + { + // Promotion + notation += "=" + move.appear[0].p.toUpperCase(); + } + return notation; + } } diff --git a/client/src/variants/Antiking.js b/client/src/variants/Antiking.js index 7b57e74a..610dd257 100644 --- a/client/src/variants/Antiking.js +++ b/client/src/variants/Antiking.js @@ -4,204 +4,204 @@ import { randInt } from "@/utils/alea"; export const VariantRules = class AntikingRules extends ChessRules { - static getPpath(b) - { - return b[1]=='a' ? "Antiking/"+b : b; - } - - static get ANTIKING() { return 'a'; } - - static get PIECES() - { - return ChessRules.PIECES.concat([V.ANTIKING]); - } - - setOtherVariables(fen) - { - super.setOtherVariables(fen); - this.antikingPos = {'w':[-1,-1], 'b':[-1,-1]}; - const rows = V.ParseFen(fen).position.split("/"); - for (let i=0; i<rows.length; i++) - { - let k = 0; - for (let j=0; j<rows[i].length; j++) - { - switch (rows[i].charAt(j)) - { - case 'a': - this.antikingPos['b'] = [i,k]; - break; - case 'A': - this.antikingPos['w'] = [i,k]; - break; - default: - const num = parseInt(rows[i].charAt(j)); - if (!isNaN(num)) - k += (num-1); - } - k++; - } - } - } - - canTake([x1,y1], [x2,y2]) - { - const piece1 = this.getPiece(x1,y1); - const piece2 = this.getPiece(x2,y2); - const color1 = this.getColor(x1,y1); - const color2 = this.getColor(x2,y2); - return piece2 != "a" && - ((piece1 != "a" && color1 != color2) || (piece1 == "a" && color1 == color2)); - } - - getPotentialMovesFrom([x,y]) - { - switch (this.getPiece(x,y)) - { - case V.ANTIKING: - return this.getPotentialAntikingMoves([x,y]); - default: - return super.getPotentialMovesFrom([x,y]); - } - } - - getPotentialAntikingMoves(sq) - { - return this.getSlideNJumpMoves(sq, - V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); - } - - isAttacked(sq, colors) - { - return (super.isAttacked(sq, colors) || this.isAttackedByAntiking(sq, colors)); - } - - isAttackedByKing([x,y], colors) - { - if (this.getPiece(x,y) == V.ANTIKING) - return false; //antiking is not attacked by king - return this.isAttackedBySlideNJump([x,y], colors, V.KING, - V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); - } - - isAttackedByAntiking([x,y], colors) - { - if ([V.KING,V.ANTIKING].includes(this.getPiece(x,y))) - return false; //(anti)king is not attacked by antiking - return this.isAttackedBySlideNJump([x,y], colors, V.ANTIKING, - V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); - } - - underCheck(color) - { - const oppCol = V.GetOppCol(color); - let res = this.isAttacked(this.kingPos[color], [oppCol]) - || !this.isAttacked(this.antikingPos[color], [oppCol]); - return res; - } - - getCheckSquares(color) - { - let res = super.getCheckSquares(color); - if (!this.isAttacked(this.antikingPos[color], [V.GetOppCol(color)])) - res.push(JSON.parse(JSON.stringify(this.antikingPos[color]))); - return res; - } - - updateVariables(move) - { - super.updateVariables(move); - const piece = move.vanish[0].p; - const c = move.vanish[0].c; - // Update antiking position - if (piece == V.ANTIKING) - { - this.antikingPos[c][0] = move.appear[0].x; - this.antikingPos[c][1] = move.appear[0].y; - } - } - - unupdateVariables(move) - { - super.unupdateVariables(move); - const c = move.vanish[0].c; - if (move.vanish[0].p == V.ANTIKING) - this.antikingPos[c] = [move.start.x, move.start.y]; - } - - getCurrentScore() - { + static getPpath(b) + { + return b[1]=='a' ? "Antiking/"+b : b; + } + + static get ANTIKING() { return 'a'; } + + static get PIECES() + { + return ChessRules.PIECES.concat([V.ANTIKING]); + } + + setOtherVariables(fen) + { + super.setOtherVariables(fen); + this.antikingPos = {'w':[-1,-1], 'b':[-1,-1]}; + const rows = V.ParseFen(fen).position.split("/"); + for (let i=0; i<rows.length; i++) + { + let k = 0; + for (let j=0; j<rows[i].length; j++) + { + switch (rows[i].charAt(j)) + { + case 'a': + this.antikingPos['b'] = [i,k]; + break; + case 'A': + this.antikingPos['w'] = [i,k]; + break; + default: + const num = parseInt(rows[i].charAt(j)); + if (!isNaN(num)) + k += (num-1); + } + k++; + } + } + } + + canTake([x1,y1], [x2,y2]) + { + const piece1 = this.getPiece(x1,y1); + const piece2 = this.getPiece(x2,y2); + const color1 = this.getColor(x1,y1); + const color2 = this.getColor(x2,y2); + return piece2 != "a" && + ((piece1 != "a" && color1 != color2) || (piece1 == "a" && color1 == color2)); + } + + getPotentialMovesFrom([x,y]) + { + switch (this.getPiece(x,y)) + { + case V.ANTIKING: + return this.getPotentialAntikingMoves([x,y]); + default: + return super.getPotentialMovesFrom([x,y]); + } + } + + getPotentialAntikingMoves(sq) + { + return this.getSlideNJumpMoves(sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + } + + isAttacked(sq, colors) + { + return (super.isAttacked(sq, colors) || this.isAttackedByAntiking(sq, colors)); + } + + isAttackedByKing([x,y], colors) + { + if (this.getPiece(x,y) == V.ANTIKING) + return false; //antiking is not attacked by king + return this.isAttackedBySlideNJump([x,y], colors, V.KING, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + } + + isAttackedByAntiking([x,y], colors) + { + if ([V.KING,V.ANTIKING].includes(this.getPiece(x,y))) + return false; //(anti)king is not attacked by antiking + return this.isAttackedBySlideNJump([x,y], colors, V.ANTIKING, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + } + + underCheck(color) + { + const oppCol = V.GetOppCol(color); + let res = this.isAttacked(this.kingPos[color], [oppCol]) + || !this.isAttacked(this.antikingPos[color], [oppCol]); + return res; + } + + getCheckSquares(color) + { + let res = super.getCheckSquares(color); + if (!this.isAttacked(this.antikingPos[color], [V.GetOppCol(color)])) + res.push(JSON.parse(JSON.stringify(this.antikingPos[color]))); + return res; + } + + updateVariables(move) + { + super.updateVariables(move); + const piece = move.vanish[0].p; + const c = move.vanish[0].c; + // Update antiking position + if (piece == V.ANTIKING) + { + this.antikingPos[c][0] = move.appear[0].x; + this.antikingPos[c][1] = move.appear[0].y; + } + } + + unupdateVariables(move) + { + super.unupdateVariables(move); + const c = move.vanish[0].c; + if (move.vanish[0].p == V.ANTIKING) + this.antikingPos[c] = [move.start.x, move.start.y]; + } + + getCurrentScore() + { if (this.atLeastOneMove()) // game not over return "*"; const color = this.turn; - const oppCol = V.GetOppCol(color); - if (!this.isAttacked(this.kingPos[color], [oppCol]) - && this.isAttacked(this.antikingPos[color], [oppCol])) - { - return "1/2"; - } - return color == "w" ? "0-1" : "1-0"; - } - - static get VALUES() { - return Object.assign( - ChessRules.VALUES, - { 'a': 1000 } - ); - } - - static GenRandInitFen() - { - let pieces = { "w": new Array(8), "b": new Array(8) }; - let antikingPos = { "w": -1, "b": -1 }; - for (let c of ["w","b"]) - { - let positions = ArrayFun.range(8); - - // Get random squares for bishops, but avoid corners; because, - // if an antiking blocks a cornered bishop, it can never be checkmated - let randIndex = 2 * randInt(1,4); - const bishop1Pos = positions[randIndex]; - let randIndex_tmp = 2 * randInt(3) + 1; - const bishop2Pos = positions[randIndex_tmp]; - positions.splice(Math.max(randIndex,randIndex_tmp), 1); - positions.splice(Math.min(randIndex,randIndex_tmp), 1); - - randIndex = randInt(6); - const knight1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - randIndex = randInt(5); - const knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - randIndex = randInt(4); - const queenPos = positions[randIndex]; - positions.splice(randIndex, 1); - - const rook1Pos = positions[0]; - const kingPos = positions[1]; - const rook2Pos = positions[2]; - - // Random squares for antikings - antikingPos[c] = randInt(8); - - pieces[c][rook1Pos] = 'r'; - pieces[c][knight1Pos] = 'n'; - pieces[c][bishop1Pos] = 'b'; - pieces[c][queenPos] = 'q'; - pieces[c][kingPos] = 'k'; - pieces[c][bishop2Pos] = 'b'; - pieces[c][knight2Pos] = 'n'; - pieces[c][rook2Pos] = 'r'; - } - const ranks23_black = "pppppppp/" + (antikingPos["w"]>0?antikingPos["w"]:"") - + "A" + (antikingPos["w"]<7?7-antikingPos["w"]:""); - const ranks23_white = (antikingPos["b"]>0?antikingPos["b"]:"") + "a" - + (antikingPos["b"]<7?7-antikingPos["b"]:"") + "/PPPPPPPP"; - return pieces["b"].join("") + "/" + ranks23_black + - "/8/8/" + - ranks23_white + "/" + pieces["w"].join("").toUpperCase() + - " w 0 1111 -"; - } + const oppCol = V.GetOppCol(color); + if (!this.isAttacked(this.kingPos[color], [oppCol]) + && this.isAttacked(this.antikingPos[color], [oppCol])) + { + return "1/2"; + } + return color == "w" ? "0-1" : "1-0"; + } + + static get VALUES() { + return Object.assign( + ChessRules.VALUES, + { 'a': 1000 } + ); + } + + static GenRandInitFen() + { + let pieces = { "w": new Array(8), "b": new Array(8) }; + let antikingPos = { "w": -1, "b": -1 }; + for (let c of ["w","b"]) + { + let positions = ArrayFun.range(8); + + // Get random squares for bishops, but avoid corners; because, + // if an antiking blocks a cornered bishop, it can never be checkmated + let randIndex = 2 * randInt(1,4); + const bishop1Pos = positions[randIndex]; + let randIndex_tmp = 2 * randInt(3) + 1; + const bishop2Pos = positions[randIndex_tmp]; + positions.splice(Math.max(randIndex,randIndex_tmp), 1); + positions.splice(Math.min(randIndex,randIndex_tmp), 1); + + randIndex = randInt(6); + const knight1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + randIndex = randInt(5); + const knight2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + randIndex = randInt(4); + const queenPos = positions[randIndex]; + positions.splice(randIndex, 1); + + const rook1Pos = positions[0]; + const kingPos = positions[1]; + const rook2Pos = positions[2]; + + // Random squares for antikings + antikingPos[c] = randInt(8); + + pieces[c][rook1Pos] = 'r'; + pieces[c][knight1Pos] = 'n'; + pieces[c][bishop1Pos] = 'b'; + pieces[c][queenPos] = 'q'; + pieces[c][kingPos] = 'k'; + pieces[c][bishop2Pos] = 'b'; + pieces[c][knight2Pos] = 'n'; + pieces[c][rook2Pos] = 'r'; + } + const ranks23_black = "pppppppp/" + (antikingPos["w"]>0?antikingPos["w"]:"") + + "A" + (antikingPos["w"]<7?7-antikingPos["w"]:""); + const ranks23_white = (antikingPos["b"]>0?antikingPos["b"]:"") + "a" + + (antikingPos["b"]<7?7-antikingPos["b"]:"") + "/PPPPPPPP"; + return pieces["b"].join("") + "/" + ranks23_black + + "/8/8/" + + ranks23_white + "/" + pieces["w"].join("").toUpperCase() + + " w 0 1111 -"; + } } diff --git a/client/src/variants/Atomic.js b/client/src/variants/Atomic.js index e1b1c161..f42d593f 100644 --- a/client/src/variants/Atomic.js +++ b/client/src/variants/Atomic.js @@ -2,145 +2,145 @@ import { ChessRules, PiPo } from "@/base_rules"; export const VariantRules = class AtomicRules extends ChessRules { - getPotentialMovesFrom([x,y]) - { - let moves = super.getPotentialMovesFrom([x,y]); + getPotentialMovesFrom([x,y]) + { + let moves = super.getPotentialMovesFrom([x,y]); - // Handle explosions - moves.forEach(m => { - if (m.vanish.length > 1 && m.appear.length <= 1) //avoid castles - { - // Explosion! TODO(?): drop moves which explode our king here - let steps = [ [-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1] ]; - for (let step of steps) - { - let x = m.end.x + step[0]; - let y = m.end.y + step[1]; - if (V.OnBoard(x,y) && this.board[x][y] != V.EMPTY - && this.getPiece(x,y) != V.PAWN) - { - m.vanish.push( - new PiPo({p:this.getPiece(x,y),c:this.getColor(x,y),x:x,y:y})); - } - } - m.end = {x:m.appear[0].x, y:m.appear[0].y}; - m.appear.pop(); //Nothin appears in this case - } - }); + // Handle explosions + moves.forEach(m => { + if (m.vanish.length > 1 && m.appear.length <= 1) //avoid castles + { + // Explosion! TODO(?): drop moves which explode our king here + let steps = [ [-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1] ]; + for (let step of steps) + { + let x = m.end.x + step[0]; + let y = m.end.y + step[1]; + if (V.OnBoard(x,y) && this.board[x][y] != V.EMPTY + && this.getPiece(x,y) != V.PAWN) + { + m.vanish.push( + new PiPo({p:this.getPiece(x,y),c:this.getColor(x,y),x:x,y:y})); + } + } + m.end = {x:m.appear[0].x, y:m.appear[0].y}; + m.appear.pop(); //Nothin appears in this case + } + }); - return moves; - } + return moves; + } - getPotentialKingMoves([x,y]) - { - // King cannot capture: - let moves = []; - const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); - for (let step of steps) - { - const i = x + step[0]; - const j = y + step[1]; - if (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) - moves.push(this.getBasicMove([x,y], [i,j])); - } - return moves.concat(this.getCastleMoves([x,y])); - } + getPotentialKingMoves([x,y]) + { + // King cannot capture: + let moves = []; + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + for (let step of steps) + { + const i = x + step[0]; + const j = y + step[1]; + if (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) + moves.push(this.getBasicMove([x,y], [i,j])); + } + return moves.concat(this.getCastleMoves([x,y])); + } - isAttacked(sq, colors) - { - if (this.getPiece(sq[0],sq[1]) == V.KING && this.isAttackedByKing(sq, colors)) - return false; //king cannot take... - return (this.isAttackedByPawn(sq, colors) - || this.isAttackedByRook(sq, colors) - || this.isAttackedByKnight(sq, colors) - || this.isAttackedByBishop(sq, colors) - || this.isAttackedByQueen(sq, colors)); - } + isAttacked(sq, colors) + { + if (this.getPiece(sq[0],sq[1]) == V.KING && this.isAttackedByKing(sq, colors)) + return false; //king cannot take... + return (this.isAttackedByPawn(sq, colors) + || this.isAttackedByRook(sq, colors) + || this.isAttackedByKnight(sq, colors) + || this.isAttackedByBishop(sq, colors) + || this.isAttackedByQueen(sq, colors)); + } - updateVariables(move) - { - super.updateVariables(move); - const color = move.vanish[0].c; - if (move.appear.length == 0) //capture - { - const firstRank = {"w": 7, "b": 0}; - for (let c of ["w","b"]) - { - // Did we explode king of color c ? (TODO: remove move earlier) - if (Math.abs(this.kingPos[c][0]-move.end.x) <= 1 - && Math.abs(this.kingPos[c][1]-move.end.y) <= 1) - { - this.kingPos[c] = [-1,-1]; - this.castleFlags[c] = [false,false]; - } - else - { - // Now check if init rook(s) exploded - if (Math.abs(move.end.x-firstRank[c]) <= 1) - { - if (Math.abs(move.end.y-this.INIT_COL_ROOK[c][0]) <= 1) - this.castleFlags[c][0] = false; - if (Math.abs(move.end.y-this.INIT_COL_ROOK[c][1]) <= 1) - this.castleFlags[c][1] = false; - } - } - } - } - } + updateVariables(move) + { + super.updateVariables(move); + const color = move.vanish[0].c; + if (move.appear.length == 0) //capture + { + const firstRank = {"w": 7, "b": 0}; + for (let c of ["w","b"]) + { + // Did we explode king of color c ? (TODO: remove move earlier) + if (Math.abs(this.kingPos[c][0]-move.end.x) <= 1 + && Math.abs(this.kingPos[c][1]-move.end.y) <= 1) + { + this.kingPos[c] = [-1,-1]; + this.castleFlags[c] = [false,false]; + } + else + { + // Now check if init rook(s) exploded + if (Math.abs(move.end.x-firstRank[c]) <= 1) + { + if (Math.abs(move.end.y-this.INIT_COL_ROOK[c][0]) <= 1) + this.castleFlags[c][0] = false; + if (Math.abs(move.end.y-this.INIT_COL_ROOK[c][1]) <= 1) + this.castleFlags[c][1] = false; + } + } + } + } + } - unupdateVariables(move) - { - super.unupdateVariables(move); - const c = move.vanish[0].c; - const oppCol = V.GetOppCol(c); - if ([this.kingPos[c][0],this.kingPos[oppCol][0]].some(e => { return e < 0; })) - { - // There is a chance that last move blowed some king away.. - for (let psq of move.vanish) - { - if (psq.p == 'k') - this.kingPos[psq.c==c ? c : oppCol] = [psq.x, psq.y]; - } - } - } + unupdateVariables(move) + { + super.unupdateVariables(move); + const c = move.vanish[0].c; + const oppCol = V.GetOppCol(c); + if ([this.kingPos[c][0],this.kingPos[oppCol][0]].some(e => { return e < 0; })) + { + // There is a chance that last move blowed some king away.. + for (let psq of move.vanish) + { + if (psq.p == 'k') + this.kingPos[psq.c==c ? c : oppCol] = [psq.x, psq.y]; + } + } + } - underCheck(color) - { - const oppCol = V.GetOppCol(color); - let res = undefined; - // If our king disappeared, move is not valid - if (this.kingPos[color][0] < 0) - res = true; - // If opponent king disappeared, move is valid - else if (this.kingPos[oppCol][0] < 0) - res = false; - // Otherwise, if we remain under check, move is not valid - else - res = this.isAttacked(this.kingPos[color], [oppCol]); - return res; - } + underCheck(color) + { + const oppCol = V.GetOppCol(color); + let res = undefined; + // If our king disappeared, move is not valid + if (this.kingPos[color][0] < 0) + res = true; + // If opponent king disappeared, move is valid + else if (this.kingPos[oppCol][0] < 0) + res = false; + // Otherwise, if we remain under check, move is not valid + else + res = this.isAttacked(this.kingPos[color], [oppCol]); + return res; + } - getCheckSquares(color) - { - let res = [ ]; - if (this.kingPos[color][0] >= 0 //king might have exploded - && this.isAttacked(this.kingPos[color], [V.GetOppCol(color)])) - { - res = [ JSON.parse(JSON.stringify(this.kingPos[color])) ] - } - return res; - } + getCheckSquares(color) + { + let res = [ ]; + if (this.kingPos[color][0] >= 0 //king might have exploded + && this.isAttacked(this.kingPos[color], [V.GetOppCol(color)])) + { + res = [ JSON.parse(JSON.stringify(this.kingPos[color])) ] + } + return res; + } - getCurrentScore() - { - const color = this.turn; - const kp = this.kingPos[color]; - if (kp[0] < 0) //king disappeared - return color == "w" ? "0-1" : "1-0"; + getCurrentScore() + { + const color = this.turn; + const kp = this.kingPos[color]; + if (kp[0] < 0) //king disappeared + return color == "w" ? "0-1" : "1-0"; if (this.atLeastOneMove()) // game not over return "*"; - if (!this.isAttacked(kp, [V.GetOppCol(color)])) - return "1/2"; - return color == "w" ? "0-1" : "1-0"; //checkmate - } + if (!this.isAttacked(kp, [V.GetOppCol(color)])) + return "1/2"; + return color == "w" ? "0-1" : "1-0"; //checkmate + } } diff --git a/client/src/variants/Baroque.js b/client/src/variants/Baroque.js index 9b5a3cdb..88c993a9 100644 --- a/client/src/variants/Baroque.js +++ b/client/src/variants/Baroque.js @@ -4,617 +4,617 @@ import { randInt } from "@/utils/alea"; export const VariantRules = class BaroqueRules extends ChessRules { - static get HasFlags() { return false; } - - static get HasEnpassant() { return false; } - - static getPpath(b) - { - if (b[1] == "m") //'m' for Immobilizer (I is too similar to 1) - return "Baroque/" + b; - return b; //usual piece - } - - static get PIECES() - { - return ChessRules.PIECES.concat([V.IMMOBILIZER]); - } - - // No castling, but checks, so keep track of kings - setOtherVariables(fen) - { - this.kingPos = {'w':[-1,-1], 'b':[-1,-1]}; - const fenParts = fen.split(" "); - const position = fenParts[0].split("/"); - for (let i=0; i<position.length; i++) - { - let k = 0; - for (let j=0; j<position[i].length; j++) - { - switch (position[i].charAt(j)) - { - case 'k': - this.kingPos['b'] = [i,k]; - break; - case 'K': - this.kingPos['w'] = [i,k]; - break; - default: - let num = parseInt(position[i].charAt(j)); - if (!isNaN(num)) - k += (num-1); - } - k++; - } - } - } - - static get IMMOBILIZER() { return 'm'; } - // Although other pieces keep their names here for coding simplicity, - // keep in mind that: - // - a "rook" is a coordinator, capturing by coordinating with the king - // - a "knight" is a long-leaper, capturing as in draughts - // - a "bishop" is a chameleon, capturing as its prey - // - a "queen" is a withdrawer, capturing by moving away from pieces - - // Is piece on square (x,y) immobilized? - isImmobilized([x,y]) - { - const piece = this.getPiece(x,y); - const color = this.getColor(x,y); - const oppCol = V.GetOppCol(color); - const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); - outerLoop: - for (let step of adjacentSteps) - { - const [i,j] = [x+step[0],y+step[1]]; - if (V.OnBoard(i,j) && this.board[i][j] != V.EMPTY - && this.getColor(i,j) == oppCol) - { - const oppPiece = this.getPiece(i,j); - if (oppPiece == V.IMMOBILIZER) - { - // Moving is impossible only if this immobilizer is not neutralized - for (let step2 of adjacentSteps) - { - const [i2,j2] = [i+step2[0],j+step2[1]]; - if (i2 == x && j2 == y) - continue; //skip initial piece! - if (V.OnBoard(i2,j2) && this.board[i2][j2] != V.EMPTY - && this.getColor(i2,j2) == color) - { - if ([V.BISHOP,V.IMMOBILIZER].includes(this.getPiece(i2,j2))) - return false; - } - } - return true; //immobilizer isn't neutralized - } - // Chameleons can't be immobilized twice, because there is only one immobilizer - if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER) - return true; - } - } - return false; - } - - getPotentialMovesFrom([x,y]) - { - // Pre-check: is thing on this square immobilized? - if (this.isImmobilized([x,y])) - return []; - switch (this.getPiece(x,y)) - { - case V.IMMOBILIZER: - return this.getPotentialImmobilizerMoves([x,y]); - default: - return super.getPotentialMovesFrom([x,y]); - } - } - - getSlideNJumpMoves([x,y], steps, oneStep) - { - const color = this.getColor(x,y); - const piece = this.getPiece(x,y); - let moves = []; - outerLoop: - for (let step of steps) - { - let i = x + step[0]; - let j = y + step[1]; - while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) - { - moves.push(this.getBasicMove([x,y], [i,j])); - if (oneStep !== undefined) - continue outerLoop; - i += step[0]; - j += step[1]; - } - // Only king can take on occupied square: - if (piece==V.KING && V.OnBoard(i,j) && this.canTake([x,y], [i,j])) - moves.push(this.getBasicMove([x,y], [i,j])); - } - return moves; - } - - // Modify capturing moves among listed pawn moves - addPawnCaptures(moves, byChameleon) - { - const steps = V.steps[V.ROOK]; - const color = this.turn; - const oppCol = V.GetOppCol(color); - moves.forEach(m => { - if (!!byChameleon && m.start.x!=m.end.x && m.start.y!=m.end.y) - return; //chameleon not moving as pawn - // Try capturing in every direction - for (let step of steps) - { - const sq2 = [m.end.x+2*step[0],m.end.y+2*step[1]]; - if (V.OnBoard(sq2[0],sq2[1]) && this.board[sq2[0]][sq2[1]] != V.EMPTY - && this.getColor(sq2[0],sq2[1]) == color) - { - // Potential capture - const sq1 = [m.end.x+step[0],m.end.y+step[1]]; - if (this.board[sq1[0]][sq1[1]] != V.EMPTY - && this.getColor(sq1[0],sq1[1]) == oppCol) - { - const piece1 = this.getPiece(sq1[0],sq1[1]); - if (!byChameleon || piece1 == V.PAWN) - { - m.vanish.push(new PiPo({ - x:sq1[0], - y:sq1[1], - c:oppCol, - p:piece1 - })); - } - } - } - } - }); - } - - // "Pincer" - getPotentialPawnMoves([x,y]) - { - let moves = super.getPotentialRookMoves([x,y]); - this.addPawnCaptures(moves); - return moves; - } - - addRookCaptures(moves, byChameleon) - { - const color = this.turn; - const oppCol = V.GetOppCol(color); - const kp = this.kingPos[color]; - moves.forEach(m => { - // Check piece-king rectangle (if any) corners for enemy pieces - if (m.end.x == kp[0] || m.end.y == kp[1]) - return; //"flat rectangle" - const corner1 = [m.end.x, kp[1]]; - const corner2 = [kp[0], m.end.y]; - for (let [i,j] of [corner1,corner2]) - { - if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == oppCol) - { - const piece = this.getPiece(i,j); - if (!byChameleon || piece == V.ROOK) - { - m.vanish.push( new PiPo({ - x:i, - y:j, - p:piece, - c:oppCol - }) ); - } - } - } - }); - } - - // Coordinator - getPotentialRookMoves(sq) - { - let moves = super.getPotentialQueenMoves(sq); - this.addRookCaptures(moves); - return moves; - } - - // Long-leaper - getKnightCaptures(startSquare, byChameleon) - { - // Look in every direction for captures - const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); - const color = this.turn; - const oppCol = V.GetOppCol(color); - let moves = []; - const [x,y] = [startSquare[0],startSquare[1]]; - const piece = this.getPiece(x,y); //might be a chameleon! - outerLoop: - for (let step of steps) - { - let [i,j] = [x+step[0], y+step[1]]; - while (V.OnBoard(i,j) && this.board[i][j]==V.EMPTY) - { - i += step[0]; - j += step[1]; - } - if (!V.OnBoard(i,j) || this.getColor(i,j)==color - || (!!byChameleon && this.getPiece(i,j)!=V.KNIGHT)) - { - continue; - } - // last(thing), cur(thing) : stop if "cur" is our color, or beyond board limits, - // or if "last" isn't empty and cur neither. Otherwise, if cur is empty then - // add move until cur square; if cur is occupied then stop if !!byChameleon and - // the square not occupied by a leaper. - let last = [i,j]; - let cur = [i+step[0],j+step[1]]; - let vanished = [ new PiPo({x:x,y:y,c:color,p:piece}) ]; - while (V.OnBoard(cur[0],cur[1])) - { - if (this.board[last[0]][last[1]] != V.EMPTY) - { - const oppPiece = this.getPiece(last[0],last[1]); - if (!!byChameleon && oppPiece != V.KNIGHT) - continue outerLoop; - // Something to eat: - vanished.push( new PiPo({x:last[0],y:last[1],c:oppCol,p:oppPiece}) ); - } - if (this.board[cur[0]][cur[1]] != V.EMPTY) - { - if (this.getColor(cur[0],cur[1]) == color - || this.board[last[0]][last[1]] != V.EMPTY) //TODO: redundant test - { - continue outerLoop; - } - } - else - { - moves.push(new Move({ - appear: [ new PiPo({x:cur[0],y:cur[1],c:color,p:piece}) ], - vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required? - start: {x:x,y:y}, - end: {x:cur[0],y:cur[1]} - })); - } - last = [last[0]+step[0],last[1]+step[1]]; - cur = [cur[0]+step[0],cur[1]+step[1]]; - } - } - return moves; - } - - // Long-leaper - getPotentialKnightMoves(sq) - { - return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq)); - } - - getPotentialBishopMoves([x,y]) - { - let moves = super.getPotentialQueenMoves([x,y]) - .concat(this.getKnightCaptures([x,y],"asChameleon")); - // No "king capture" because king cannot remain under check - this.addPawnCaptures(moves, "asChameleon"); - this.addRookCaptures(moves, "asChameleon"); - this.addQueenCaptures(moves, "asChameleon"); - // Post-processing: merge similar moves, concatenating vanish arrays - let mergedMoves = {}; - moves.forEach(m => { - const key = m.end.x + V.size.x * m.end.y; - if (!mergedMoves[key]) - mergedMoves[key] = m; - else - { - for (let i=1; i<m.vanish.length; i++) - mergedMoves[key].vanish.push(m.vanish[i]); - } - }); - // Finally return an array - moves = []; - Object.keys(mergedMoves).forEach(k => { moves.push(mergedMoves[k]); }); - return moves; - } - - // Withdrawer - addQueenCaptures(moves, byChameleon) - { - if (moves.length == 0) - return; - const [x,y] = [moves[0].start.x,moves[0].start.y]; - const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); - let capturingDirections = []; - const color = this.turn; - const oppCol = V.GetOppCol(color); - adjacentSteps.forEach(step => { - const [i,j] = [x+step[0],y+step[1]]; - if (V.OnBoard(i,j) && this.board[i][j] != V.EMPTY && this.getColor(i,j) == oppCol - && (!byChameleon || this.getPiece(i,j) == V.QUEEN)) - { - capturingDirections.push(step); - } - }); - moves.forEach(m => { - const step = [ - m.end.x!=x ? (m.end.x-x)/Math.abs(m.end.x-x) : 0, - m.end.y!=y ? (m.end.y-y)/Math.abs(m.end.y-y) : 0 - ]; - // NOTE: includes() and even _.isEqual() functions fail... - // TODO: this test should be done only once per direction - if (capturingDirections.some(dir => - { return (dir[0]==-step[0] && dir[1]==-step[1]); })) - { - const [i,j] = [x-step[0],y-step[1]]; - m.vanish.push(new PiPo({ - x:i, - y:j, - p:this.getPiece(i,j), - c:oppCol - })); - } - }); - } - - getPotentialQueenMoves(sq) - { - let moves = super.getPotentialQueenMoves(sq); - this.addQueenCaptures(moves); - return moves; - } - - getPotentialImmobilizerMoves(sq) - { - // Immobilizer doesn't capture - return super.getPotentialQueenMoves(sq); - } - - getPotentialKingMoves(sq) - { - return this.getSlideNJumpMoves(sq, - V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); - } - - // isAttacked() is OK because the immobilizer doesn't take - - isAttackedByPawn([x,y], colors) - { - // Square (x,y) must be surroundable by two enemy pieces, - // and one of them at least should be a pawn (moving). - const dirs = [ [1,0],[0,1] ]; - const steps = V.steps[V.ROOK]; - for (let dir of dirs) - { - const [i1,j1] = [x-dir[0],y-dir[1]]; //"before" - const [i2,j2] = [x+dir[0],y+dir[1]]; //"after" - if (V.OnBoard(i1,j1) && V.OnBoard(i2,j2)) - { - if ((this.board[i1][j1]!=V.EMPTY && colors.includes(this.getColor(i1,j1)) - && this.board[i2][j2]==V.EMPTY) - || - (this.board[i2][j2]!=V.EMPTY && colors.includes(this.getColor(i2,j2)) - && this.board[i1][j1]==V.EMPTY)) - { - // Search a movable enemy pawn landing on the empty square - for (let step of steps) - { - let [ii,jj] = (this.board[i1][j1]==V.EMPTY ? [i1,j1] : [i2,j2]); - let [i3,j3] = [ii+step[0],jj+step[1]]; - while (V.OnBoard(i3,j3) && this.board[i3][j3]==V.EMPTY) - { - i3 += step[0]; - j3 += step[1]; - } - if (V.OnBoard(i3,j3) && colors.includes(this.getColor(i3,j3)) - && this.getPiece(i3,j3) == V.PAWN && !this.isImmobilized([i3,j3])) - { - return true; - } - } - } - } - } - return false; - } - - isAttackedByRook([x,y], colors) - { - // King must be on same column or row, - // and a rook should be able to reach a capturing square - // colors contains only one element, giving the oppCol and thus king position - const sameRow = (x == this.kingPos[colors[0]][0]); - const sameColumn = (y == this.kingPos[colors[0]][1]); - if (sameRow || sameColumn) - { - // Look for the enemy rook (maximum 1) - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - { - if (this.board[i][j] != V.EMPTY && colors.includes(this.getColor(i,j)) - && this.getPiece(i,j) == V.ROOK) - { - if (this.isImmobilized([i,j])) - return false; //because only one rook - // Can it reach a capturing square? - // Easy but quite suboptimal way (TODO): generate all moves (turn is OK) - const moves = this.getPotentialMovesFrom([i,j]); - for (let move of moves) - { - if (sameRow && move.end.y == y || sameColumn && move.end.x == x) - return true; - } - } - } - } - } - return false; - } - - isAttackedByKnight([x,y], colors) - { - // Square (x,y) must be on same line as a knight, - // and there must be empty square(s) behind. - const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); - outerLoop: - for (let step of steps) - { - const [i0,j0] = [x+step[0],y+step[1]]; - if (V.OnBoard(i0,j0) && this.board[i0][j0] == V.EMPTY) - { - // Try in opposite direction: - let [i,j] = [x-step[0],y-step[1]]; - while (V.OnBoard(i,j)) - { - while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) - { - i -= step[0]; - j -= step[1]; - } - if (V.OnBoard(i,j)) - { - if (colors.includes(this.getColor(i,j))) - { - if (this.getPiece(i,j) == V.KNIGHT && !this.isImmobilized([i,j])) - return true; - continue outerLoop; - } - // [else] Our color, could be captured *if there was an empty space* - if (this.board[i+step[0]][j+step[1]] != V.EMPTY) - continue outerLoop; - i -= step[0]; - j -= step[1]; - } - } - } - } - return false; - } - - isAttackedByBishop([x,y], colors) - { - // We cheat a little here: since this function is used exclusively for king, - // it's enough to check the immediate surrounding of the square. - const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); - for (let step of adjacentSteps) - { - const [i,j] = [x+step[0],y+step[1]]; - if (V.OnBoard(i,j) && this.board[i][j]!=V.EMPTY - && colors.includes(this.getColor(i,j)) && this.getPiece(i,j) == V.BISHOP) - { - return true; //bishops are never immobilized - } - } - return false; - } - - isAttackedByQueen([x,y], colors) - { - // Square (x,y) must be adjacent to a queen, and the queen must have - // some free space in the opposite direction from (x,y) - const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); - for (let step of adjacentSteps) - { - const sq2 = [x+2*step[0],y+2*step[1]]; - if (V.OnBoard(sq2[0],sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY) - { - const sq1 = [x+step[0],y+step[1]]; - if (this.board[sq1[0]][sq1[1]] != V.EMPTY - && colors.includes(this.getColor(sq1[0],sq1[1])) - && this.getPiece(sq1[0],sq1[1]) == V.QUEEN - && !this.isImmobilized(sq1)) - { - return true; - } - } - } - return false; - } - - static get VALUES() - { - // TODO: totally experimental! - return { - 'p': 1, - 'r': 2, - 'n': 5, - 'b': 3, - 'q': 3, - 'm': 5, - 'k': 1000 - }; - } - - static get SEARCH_DEPTH() { return 2; } //TODO? - - static GenRandInitFen() - { - let pieces = { "w": new Array(8), "b": new Array(8) }; - // Shuffle pieces on first and last rank - for (let c of ["w","b"]) - { - let positions = ArrayFun.range(8); - // Get random squares for every piece, totally freely - - let randIndex = randInt(8); - const bishop1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - randIndex = randInt(7); - const bishop2Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - randIndex = randInt(6); - const knight1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - randIndex = randInt(5); - const knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - randIndex = randInt(4); - const queenPos = positions[randIndex]; - positions.splice(randIndex, 1); - - randIndex = randInt(3); - const kingPos = positions[randIndex]; - positions.splice(randIndex, 1); - - randIndex = randInt(2); - const rookPos = positions[randIndex]; - positions.splice(randIndex, 1); - const immobilizerPos = positions[0]; - - pieces[c][bishop1Pos] = 'b'; - pieces[c][bishop2Pos] = 'b'; - pieces[c][knight1Pos] = 'n'; - pieces[c][knight2Pos] = 'n'; - pieces[c][queenPos] = 'q'; - pieces[c][kingPos] = 'k'; - pieces[c][rookPos] = 'r'; - pieces[c][immobilizerPos] = 'm'; - } - return pieces["b"].join("") + - "/pppppppp/8/8/8/8/PPPPPPPP/" + - pieces["w"].join("").toUpperCase() + - " w 0"; - } - - getNotation(move) - { - const initialSquare = V.CoordsToSquare(move.start); - const finalSquare = V.CoordsToSquare(move.end); - let notation = undefined; - if (move.appear[0].p == V.PAWN) - { - // Pawn: generally ambiguous short notation, so we use full description - notation = "P" + initialSquare + finalSquare; - } - else if (move.appear[0].p == V.KING) - notation = "K" + (move.vanish.length>1 ? "x" : "") + finalSquare; - else - notation = move.appear[0].p.toUpperCase() + finalSquare; - if (move.vanish.length > 1 && move.appear[0].p != V.KING) - notation += "X"; //capture mark (not describing what is captured...) - return notation; - } + static get HasFlags() { return false; } + + static get HasEnpassant() { return false; } + + static getPpath(b) + { + if (b[1] == "m") //'m' for Immobilizer (I is too similar to 1) + return "Baroque/" + b; + return b; //usual piece + } + + static get PIECES() + { + return ChessRules.PIECES.concat([V.IMMOBILIZER]); + } + + // No castling, but checks, so keep track of kings + setOtherVariables(fen) + { + this.kingPos = {'w':[-1,-1], 'b':[-1,-1]}; + const fenParts = fen.split(" "); + const position = fenParts[0].split("/"); + for (let i=0; i<position.length; i++) + { + let k = 0; + for (let j=0; j<position[i].length; j++) + { + switch (position[i].charAt(j)) + { + case 'k': + this.kingPos['b'] = [i,k]; + break; + case 'K': + this.kingPos['w'] = [i,k]; + break; + default: + let num = parseInt(position[i].charAt(j)); + if (!isNaN(num)) + k += (num-1); + } + k++; + } + } + } + + static get IMMOBILIZER() { return 'm'; } + // Although other pieces keep their names here for coding simplicity, + // keep in mind that: + // - a "rook" is a coordinator, capturing by coordinating with the king + // - a "knight" is a long-leaper, capturing as in draughts + // - a "bishop" is a chameleon, capturing as its prey + // - a "queen" is a withdrawer, capturing by moving away from pieces + + // Is piece on square (x,y) immobilized? + isImmobilized([x,y]) + { + const piece = this.getPiece(x,y); + const color = this.getColor(x,y); + const oppCol = V.GetOppCol(color); + const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + outerLoop: + for (let step of adjacentSteps) + { + const [i,j] = [x+step[0],y+step[1]]; + if (V.OnBoard(i,j) && this.board[i][j] != V.EMPTY + && this.getColor(i,j) == oppCol) + { + const oppPiece = this.getPiece(i,j); + if (oppPiece == V.IMMOBILIZER) + { + // Moving is impossible only if this immobilizer is not neutralized + for (let step2 of adjacentSteps) + { + const [i2,j2] = [i+step2[0],j+step2[1]]; + if (i2 == x && j2 == y) + continue; //skip initial piece! + if (V.OnBoard(i2,j2) && this.board[i2][j2] != V.EMPTY + && this.getColor(i2,j2) == color) + { + if ([V.BISHOP,V.IMMOBILIZER].includes(this.getPiece(i2,j2))) + return false; + } + } + return true; //immobilizer isn't neutralized + } + // Chameleons can't be immobilized twice, because there is only one immobilizer + if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER) + return true; + } + } + return false; + } + + getPotentialMovesFrom([x,y]) + { + // Pre-check: is thing on this square immobilized? + if (this.isImmobilized([x,y])) + return []; + switch (this.getPiece(x,y)) + { + case V.IMMOBILIZER: + return this.getPotentialImmobilizerMoves([x,y]); + default: + return super.getPotentialMovesFrom([x,y]); + } + } + + getSlideNJumpMoves([x,y], steps, oneStep) + { + const color = this.getColor(x,y); + const piece = this.getPiece(x,y); + let moves = []; + outerLoop: + for (let step of steps) + { + let i = x + step[0]; + let j = y + step[1]; + while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) + { + moves.push(this.getBasicMove([x,y], [i,j])); + if (oneStep !== undefined) + continue outerLoop; + i += step[0]; + j += step[1]; + } + // Only king can take on occupied square: + if (piece==V.KING && V.OnBoard(i,j) && this.canTake([x,y], [i,j])) + moves.push(this.getBasicMove([x,y], [i,j])); + } + return moves; + } + + // Modify capturing moves among listed pawn moves + addPawnCaptures(moves, byChameleon) + { + const steps = V.steps[V.ROOK]; + const color = this.turn; + const oppCol = V.GetOppCol(color); + moves.forEach(m => { + if (!!byChameleon && m.start.x!=m.end.x && m.start.y!=m.end.y) + return; //chameleon not moving as pawn + // Try capturing in every direction + for (let step of steps) + { + const sq2 = [m.end.x+2*step[0],m.end.y+2*step[1]]; + if (V.OnBoard(sq2[0],sq2[1]) && this.board[sq2[0]][sq2[1]] != V.EMPTY + && this.getColor(sq2[0],sq2[1]) == color) + { + // Potential capture + const sq1 = [m.end.x+step[0],m.end.y+step[1]]; + if (this.board[sq1[0]][sq1[1]] != V.EMPTY + && this.getColor(sq1[0],sq1[1]) == oppCol) + { + const piece1 = this.getPiece(sq1[0],sq1[1]); + if (!byChameleon || piece1 == V.PAWN) + { + m.vanish.push(new PiPo({ + x:sq1[0], + y:sq1[1], + c:oppCol, + p:piece1 + })); + } + } + } + } + }); + } + + // "Pincer" + getPotentialPawnMoves([x,y]) + { + let moves = super.getPotentialRookMoves([x,y]); + this.addPawnCaptures(moves); + return moves; + } + + addRookCaptures(moves, byChameleon) + { + const color = this.turn; + const oppCol = V.GetOppCol(color); + const kp = this.kingPos[color]; + moves.forEach(m => { + // Check piece-king rectangle (if any) corners for enemy pieces + if (m.end.x == kp[0] || m.end.y == kp[1]) + return; //"flat rectangle" + const corner1 = [m.end.x, kp[1]]; + const corner2 = [kp[0], m.end.y]; + for (let [i,j] of [corner1,corner2]) + { + if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == oppCol) + { + const piece = this.getPiece(i,j); + if (!byChameleon || piece == V.ROOK) + { + m.vanish.push( new PiPo({ + x:i, + y:j, + p:piece, + c:oppCol + }) ); + } + } + } + }); + } + + // Coordinator + getPotentialRookMoves(sq) + { + let moves = super.getPotentialQueenMoves(sq); + this.addRookCaptures(moves); + return moves; + } + + // Long-leaper + getKnightCaptures(startSquare, byChameleon) + { + // Look in every direction for captures + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + const color = this.turn; + const oppCol = V.GetOppCol(color); + let moves = []; + const [x,y] = [startSquare[0],startSquare[1]]; + const piece = this.getPiece(x,y); //might be a chameleon! + outerLoop: + for (let step of steps) + { + let [i,j] = [x+step[0], y+step[1]]; + while (V.OnBoard(i,j) && this.board[i][j]==V.EMPTY) + { + i += step[0]; + j += step[1]; + } + if (!V.OnBoard(i,j) || this.getColor(i,j)==color + || (!!byChameleon && this.getPiece(i,j)!=V.KNIGHT)) + { + continue; + } + // last(thing), cur(thing) : stop if "cur" is our color, or beyond board limits, + // or if "last" isn't empty and cur neither. Otherwise, if cur is empty then + // add move until cur square; if cur is occupied then stop if !!byChameleon and + // the square not occupied by a leaper. + let last = [i,j]; + let cur = [i+step[0],j+step[1]]; + let vanished = [ new PiPo({x:x,y:y,c:color,p:piece}) ]; + while (V.OnBoard(cur[0],cur[1])) + { + if (this.board[last[0]][last[1]] != V.EMPTY) + { + const oppPiece = this.getPiece(last[0],last[1]); + if (!!byChameleon && oppPiece != V.KNIGHT) + continue outerLoop; + // Something to eat: + vanished.push( new PiPo({x:last[0],y:last[1],c:oppCol,p:oppPiece}) ); + } + if (this.board[cur[0]][cur[1]] != V.EMPTY) + { + if (this.getColor(cur[0],cur[1]) == color + || this.board[last[0]][last[1]] != V.EMPTY) //TODO: redundant test + { + continue outerLoop; + } + } + else + { + moves.push(new Move({ + appear: [ new PiPo({x:cur[0],y:cur[1],c:color,p:piece}) ], + vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required? + start: {x:x,y:y}, + end: {x:cur[0],y:cur[1]} + })); + } + last = [last[0]+step[0],last[1]+step[1]]; + cur = [cur[0]+step[0],cur[1]+step[1]]; + } + } + return moves; + } + + // Long-leaper + getPotentialKnightMoves(sq) + { + return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq)); + } + + getPotentialBishopMoves([x,y]) + { + let moves = super.getPotentialQueenMoves([x,y]) + .concat(this.getKnightCaptures([x,y],"asChameleon")); + // No "king capture" because king cannot remain under check + this.addPawnCaptures(moves, "asChameleon"); + this.addRookCaptures(moves, "asChameleon"); + this.addQueenCaptures(moves, "asChameleon"); + // Post-processing: merge similar moves, concatenating vanish arrays + let mergedMoves = {}; + moves.forEach(m => { + const key = m.end.x + V.size.x * m.end.y; + if (!mergedMoves[key]) + mergedMoves[key] = m; + else + { + for (let i=1; i<m.vanish.length; i++) + mergedMoves[key].vanish.push(m.vanish[i]); + } + }); + // Finally return an array + moves = []; + Object.keys(mergedMoves).forEach(k => { moves.push(mergedMoves[k]); }); + return moves; + } + + // Withdrawer + addQueenCaptures(moves, byChameleon) + { + if (moves.length == 0) + return; + const [x,y] = [moves[0].start.x,moves[0].start.y]; + const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + let capturingDirections = []; + const color = this.turn; + const oppCol = V.GetOppCol(color); + adjacentSteps.forEach(step => { + const [i,j] = [x+step[0],y+step[1]]; + if (V.OnBoard(i,j) && this.board[i][j] != V.EMPTY && this.getColor(i,j) == oppCol + && (!byChameleon || this.getPiece(i,j) == V.QUEEN)) + { + capturingDirections.push(step); + } + }); + moves.forEach(m => { + const step = [ + m.end.x!=x ? (m.end.x-x)/Math.abs(m.end.x-x) : 0, + m.end.y!=y ? (m.end.y-y)/Math.abs(m.end.y-y) : 0 + ]; + // NOTE: includes() and even _.isEqual() functions fail... + // TODO: this test should be done only once per direction + if (capturingDirections.some(dir => + { return (dir[0]==-step[0] && dir[1]==-step[1]); })) + { + const [i,j] = [x-step[0],y-step[1]]; + m.vanish.push(new PiPo({ + x:i, + y:j, + p:this.getPiece(i,j), + c:oppCol + })); + } + }); + } + + getPotentialQueenMoves(sq) + { + let moves = super.getPotentialQueenMoves(sq); + this.addQueenCaptures(moves); + return moves; + } + + getPotentialImmobilizerMoves(sq) + { + // Immobilizer doesn't capture + return super.getPotentialQueenMoves(sq); + } + + getPotentialKingMoves(sq) + { + return this.getSlideNJumpMoves(sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + } + + // isAttacked() is OK because the immobilizer doesn't take + + isAttackedByPawn([x,y], colors) + { + // Square (x,y) must be surroundable by two enemy pieces, + // and one of them at least should be a pawn (moving). + const dirs = [ [1,0],[0,1] ]; + const steps = V.steps[V.ROOK]; + for (let dir of dirs) + { + const [i1,j1] = [x-dir[0],y-dir[1]]; //"before" + const [i2,j2] = [x+dir[0],y+dir[1]]; //"after" + if (V.OnBoard(i1,j1) && V.OnBoard(i2,j2)) + { + if ((this.board[i1][j1]!=V.EMPTY && colors.includes(this.getColor(i1,j1)) + && this.board[i2][j2]==V.EMPTY) + || + (this.board[i2][j2]!=V.EMPTY && colors.includes(this.getColor(i2,j2)) + && this.board[i1][j1]==V.EMPTY)) + { + // Search a movable enemy pawn landing on the empty square + for (let step of steps) + { + let [ii,jj] = (this.board[i1][j1]==V.EMPTY ? [i1,j1] : [i2,j2]); + let [i3,j3] = [ii+step[0],jj+step[1]]; + while (V.OnBoard(i3,j3) && this.board[i3][j3]==V.EMPTY) + { + i3 += step[0]; + j3 += step[1]; + } + if (V.OnBoard(i3,j3) && colors.includes(this.getColor(i3,j3)) + && this.getPiece(i3,j3) == V.PAWN && !this.isImmobilized([i3,j3])) + { + return true; + } + } + } + } + } + return false; + } + + isAttackedByRook([x,y], colors) + { + // King must be on same column or row, + // and a rook should be able to reach a capturing square + // colors contains only one element, giving the oppCol and thus king position + const sameRow = (x == this.kingPos[colors[0]][0]); + const sameColumn = (y == this.kingPos[colors[0]][1]); + if (sameRow || sameColumn) + { + // Look for the enemy rook (maximum 1) + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (this.board[i][j] != V.EMPTY && colors.includes(this.getColor(i,j)) + && this.getPiece(i,j) == V.ROOK) + { + if (this.isImmobilized([i,j])) + return false; //because only one rook + // Can it reach a capturing square? + // Easy but quite suboptimal way (TODO): generate all moves (turn is OK) + const moves = this.getPotentialMovesFrom([i,j]); + for (let move of moves) + { + if (sameRow && move.end.y == y || sameColumn && move.end.x == x) + return true; + } + } + } + } + } + return false; + } + + isAttackedByKnight([x,y], colors) + { + // Square (x,y) must be on same line as a knight, + // and there must be empty square(s) behind. + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + outerLoop: + for (let step of steps) + { + const [i0,j0] = [x+step[0],y+step[1]]; + if (V.OnBoard(i0,j0) && this.board[i0][j0] == V.EMPTY) + { + // Try in opposite direction: + let [i,j] = [x-step[0],y-step[1]]; + while (V.OnBoard(i,j)) + { + while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) + { + i -= step[0]; + j -= step[1]; + } + if (V.OnBoard(i,j)) + { + if (colors.includes(this.getColor(i,j))) + { + if (this.getPiece(i,j) == V.KNIGHT && !this.isImmobilized([i,j])) + return true; + continue outerLoop; + } + // [else] Our color, could be captured *if there was an empty space* + if (this.board[i+step[0]][j+step[1]] != V.EMPTY) + continue outerLoop; + i -= step[0]; + j -= step[1]; + } + } + } + } + return false; + } + + isAttackedByBishop([x,y], colors) + { + // We cheat a little here: since this function is used exclusively for king, + // it's enough to check the immediate surrounding of the square. + const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + for (let step of adjacentSteps) + { + const [i,j] = [x+step[0],y+step[1]]; + if (V.OnBoard(i,j) && this.board[i][j]!=V.EMPTY + && colors.includes(this.getColor(i,j)) && this.getPiece(i,j) == V.BISHOP) + { + return true; //bishops are never immobilized + } + } + return false; + } + + isAttackedByQueen([x,y], colors) + { + // Square (x,y) must be adjacent to a queen, and the queen must have + // some free space in the opposite direction from (x,y) + const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + for (let step of adjacentSteps) + { + const sq2 = [x+2*step[0],y+2*step[1]]; + if (V.OnBoard(sq2[0],sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY) + { + const sq1 = [x+step[0],y+step[1]]; + if (this.board[sq1[0]][sq1[1]] != V.EMPTY + && colors.includes(this.getColor(sq1[0],sq1[1])) + && this.getPiece(sq1[0],sq1[1]) == V.QUEEN + && !this.isImmobilized(sq1)) + { + return true; + } + } + } + return false; + } + + static get VALUES() + { + // TODO: totally experimental! + return { + 'p': 1, + 'r': 2, + 'n': 5, + 'b': 3, + 'q': 3, + 'm': 5, + 'k': 1000 + }; + } + + static get SEARCH_DEPTH() { return 2; } //TODO? + + static GenRandInitFen() + { + let pieces = { "w": new Array(8), "b": new Array(8) }; + // Shuffle pieces on first and last rank + for (let c of ["w","b"]) + { + let positions = ArrayFun.range(8); + // Get random squares for every piece, totally freely + + let randIndex = randInt(8); + const bishop1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + randIndex = randInt(7); + const bishop2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + randIndex = randInt(6); + const knight1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + randIndex = randInt(5); + const knight2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + randIndex = randInt(4); + const queenPos = positions[randIndex]; + positions.splice(randIndex, 1); + + randIndex = randInt(3); + const kingPos = positions[randIndex]; + positions.splice(randIndex, 1); + + randIndex = randInt(2); + const rookPos = positions[randIndex]; + positions.splice(randIndex, 1); + const immobilizerPos = positions[0]; + + pieces[c][bishop1Pos] = 'b'; + pieces[c][bishop2Pos] = 'b'; + pieces[c][knight1Pos] = 'n'; + pieces[c][knight2Pos] = 'n'; + pieces[c][queenPos] = 'q'; + pieces[c][kingPos] = 'k'; + pieces[c][rookPos] = 'r'; + pieces[c][immobilizerPos] = 'm'; + } + return pieces["b"].join("") + + "/pppppppp/8/8/8/8/PPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + " w 0"; + } + + getNotation(move) + { + const initialSquare = V.CoordsToSquare(move.start); + const finalSquare = V.CoordsToSquare(move.end); + let notation = undefined; + if (move.appear[0].p == V.PAWN) + { + // Pawn: generally ambiguous short notation, so we use full description + notation = "P" + initialSquare + finalSquare; + } + else if (move.appear[0].p == V.KING) + notation = "K" + (move.vanish.length>1 ? "x" : "") + finalSquare; + else + notation = move.appear[0].p.toUpperCase() + finalSquare; + if (move.vanish.length > 1 && move.appear[0].p != V.KING) + notation += "X"; //capture mark (not describing what is captured...) + return notation; + } } diff --git a/client/src/variants/Berolina.js b/client/src/variants/Berolina.js index 05152b43..592b25a6 100644 --- a/client/src/variants/Berolina.js +++ b/client/src/variants/Berolina.js @@ -2,136 +2,136 @@ import { ChessRules } from "@/base_rules"; export const VariantRules = class BerolinaRules extends ChessRules { - // En-passant after 2-sq jump - getEpSquare(moveOrSquare) - { - if (!moveOrSquare) - return undefined; - if (typeof moveOrSquare === "string") - { - const square = moveOrSquare; - if (square == "-") - return undefined; - // Enemy pawn initial column must be given too: - let res = []; - const epParts = square.split(","); - res.push(V.SquareToCoords(epParts[0])); - res.push(V.ColumnToCoord(epParts[1])); - return res; - } - // Argument is a move: - const move = moveOrSquare; - const [sx,ex,sy] = [move.start.x,move.end.x,move.start.y]; - if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) == 2) - { - return - [ - { - x: (ex + sx)/2, - y: (move.end.y + sy)/2 - }, - move.end.y - ]; - } - return undefined; //default - } + // En-passant after 2-sq jump + getEpSquare(moveOrSquare) + { + if (!moveOrSquare) + return undefined; + if (typeof moveOrSquare === "string") + { + const square = moveOrSquare; + if (square == "-") + return undefined; + // Enemy pawn initial column must be given too: + let res = []; + const epParts = square.split(","); + res.push(V.SquareToCoords(epParts[0])); + res.push(V.ColumnToCoord(epParts[1])); + return res; + } + // Argument is a move: + const move = moveOrSquare; + const [sx,ex,sy] = [move.start.x,move.end.x,move.start.y]; + if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) == 2) + { + return + [ + { + x: (ex + sx)/2, + y: (move.end.y + sy)/2 + }, + move.end.y + ]; + } + return undefined; //default + } - // Special pawns movements - getPotentialPawnMoves([x,y]) - { - const color = this.turn; - let moves = []; - const [sizeX,sizeY] = [V.size.x,V.size.y]; - const shiftX = (color == "w" ? -1 : 1); - const firstRank = (color == 'w' ? sizeX-1 : 0); - const startRank = (color == "w" ? sizeX-2 : 1); - const lastRank = (color == "w" ? 0 : sizeX-1); - const finalPieces = x + shiftX == lastRank - ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN] - : [V.PAWN]; + // Special pawns movements + getPotentialPawnMoves([x,y]) + { + const color = this.turn; + let moves = []; + const [sizeX,sizeY] = [V.size.x,V.size.y]; + const shiftX = (color == "w" ? -1 : 1); + const firstRank = (color == 'w' ? sizeX-1 : 0); + const startRank = (color == "w" ? sizeX-2 : 1); + const lastRank = (color == "w" ? 0 : sizeX-1); + const finalPieces = x + shiftX == lastRank + ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN] + : [V.PAWN]; - // One square diagonally - for (let shiftY of [-1,1]) - { - if (this.board[x+shiftX][y+shiftY] == V.EMPTY) - { - for (let piece of finalPieces) - { - moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], - {c:color,p:piece})); - } - if (x == startRank && y+2*shiftY>=0 && y+2*shiftY<sizeY - && this.board[x+2*shiftX][y+2*shiftY] == V.EMPTY) - { - // Two squares jump - moves.push(this.getBasicMove([x,y], [x+2*shiftX,y+2*shiftY])); - } - } - } - // Capture - if (this.board[x+shiftX][y] != V.EMPTY - && this.canTake([x,y], [x+shiftX,y])) - { - for (let piece of finalPieces) - moves.push(this.getBasicMove([x,y], [x+shiftX,y], {c:color,p:piece})); - } + // One square diagonally + for (let shiftY of [-1,1]) + { + if (this.board[x+shiftX][y+shiftY] == V.EMPTY) + { + for (let piece of finalPieces) + { + moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], + {c:color,p:piece})); + } + if (x == startRank && y+2*shiftY>=0 && y+2*shiftY<sizeY + && this.board[x+2*shiftX][y+2*shiftY] == V.EMPTY) + { + // Two squares jump + moves.push(this.getBasicMove([x,y], [x+2*shiftX,y+2*shiftY])); + } + } + } + // Capture + if (this.board[x+shiftX][y] != V.EMPTY + && this.canTake([x,y], [x+shiftX,y])) + { + for (let piece of finalPieces) + moves.push(this.getBasicMove([x,y], [x+shiftX,y], {c:color,p:piece})); + } - // En passant - const Lep = this.epSquares.length; - const epSquare = this.epSquares[Lep-1]; //always at least one element - if (!!epSquare && epSquare[0].x == x+shiftX && epSquare[0].y == y - && Math.abs(epSquare[1] - y) == 1) - { - let enpassantMove = this.getBasicMove([x,y], [x+shiftX,y]); - enpassantMove.vanish.push({ - x: x, - y: epSquare[1], - p: 'p', - c: this.getColor(x,epSquare[1]) - }); - moves.push(enpassantMove); - } + // En passant + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep-1]; //always at least one element + if (!!epSquare && epSquare[0].x == x+shiftX && epSquare[0].y == y + && Math.abs(epSquare[1] - y) == 1) + { + let enpassantMove = this.getBasicMove([x,y], [x+shiftX,y]); + enpassantMove.vanish.push({ + x: x, + y: epSquare[1], + p: 'p', + c: this.getColor(x,epSquare[1]) + }); + moves.push(enpassantMove); + } - return moves; - } + return moves; + } - isAttackedByPawn([x,y], colors) - { - for (let c of colors) - { - let pawnShift = (c=="w" ? 1 : -1); - if (x+pawnShift>=0 && x+pawnShift<V.size.x) - { - if (this.getPiece(x+pawnShift,y)==V.PAWN - && this.getColor(x+pawnShift,y)==c) - { - return true; - } - } - } - return false; - } + isAttackedByPawn([x,y], colors) + { + for (let c of colors) + { + let pawnShift = (c=="w" ? 1 : -1); + if (x+pawnShift>=0 && x+pawnShift<V.size.x) + { + if (this.getPiece(x+pawnShift,y)==V.PAWN + && this.getColor(x+pawnShift,y)==c) + { + return true; + } + } + } + return false; + } - getNotation(move) - { - const piece = this.getPiece(move.start.x, move.start.y); - if (piece == V.PAWN) - { - // Pawn move - const finalSquare = V.CoordsToSquare(move.end); - let notation = ""; - if (move.vanish.length == 2) //capture - notation = "Px" + finalSquare; - else - { - // No capture: indicate the initial square for potential ambiguity - const startSquare = V.CoordsToSquare(move.start); - notation = startSquare + finalSquare; - } - if (move.appear[0].p != V.PAWN) //promotion - notation += "=" + move.appear[0].p.toUpperCase(); - return notation; - } - return super.getNotation(move); //all other pieces are orthodox - } + getNotation(move) + { + const piece = this.getPiece(move.start.x, move.start.y); + if (piece == V.PAWN) + { + // Pawn move + const finalSquare = V.CoordsToSquare(move.end); + let notation = ""; + if (move.vanish.length == 2) //capture + notation = "Px" + finalSquare; + else + { + // No capture: indicate the initial square for potential ambiguity + const startSquare = V.CoordsToSquare(move.start); + notation = startSquare + finalSquare; + } + if (move.appear[0].p != V.PAWN) //promotion + notation += "=" + move.appear[0].p.toUpperCase(); + return notation; + } + return super.getNotation(move); //all other pieces are orthodox + } } diff --git a/client/src/variants/Checkered.js b/client/src/variants/Checkered.js index 586c88ee..afbf21e2 100644 --- a/client/src/variants/Checkered.js +++ b/client/src/variants/Checkered.js @@ -2,49 +2,49 @@ import { ChessRules } from "@/base_rules"; export const VariantRules = class CheckeredRules extends ChessRules { - static getPpath(b) - { - return b[0]=='c' ? "Checkered/"+b : b; - } - - static board2fen(b) - { - const checkered_codes = { - 'p': 's', - 'q': 't', - 'r': 'u', - 'b': 'c', - 'n': 'o', - }; - if (b[0]=="c") - return checkered_codes[b[1]]; - return ChessRules.board2fen(b); - } - - static fen2board(f) - { - // Tolerate upper-case versions of checkered pieces (why not?) - const checkered_pieces = { - 's': 'p', - 'S': 'p', - 't': 'q', - 'T': 'q', - 'u': 'r', - 'U': 'r', - 'c': 'b', - 'C': 'b', - 'o': 'n', - 'O': 'n', - }; - if (Object.keys(checkered_pieces).includes(f)) - return 'c'+checkered_pieces[f]; - return ChessRules.fen2board(f); - } - - static get PIECES() - { - return ChessRules.PIECES.concat(['s','t','u','c','o']); - } + static getPpath(b) + { + return b[0]=='c' ? "Checkered/"+b : b; + } + + static board2fen(b) + { + const checkered_codes = { + 'p': 's', + 'q': 't', + 'r': 'u', + 'b': 'c', + 'n': 'o', + }; + if (b[0]=="c") + return checkered_codes[b[1]]; + return ChessRules.board2fen(b); + } + + static fen2board(f) + { + // Tolerate upper-case versions of checkered pieces (why not?) + const checkered_pieces = { + 's': 'p', + 'S': 'p', + 't': 'q', + 'T': 'q', + 'u': 'r', + 'U': 'r', + 'c': 'b', + 'C': 'b', + 'o': 'n', + 'O': 'n', + }; + if (Object.keys(checkered_pieces).includes(f)) + return 'c'+checkered_pieces[f]; + return ChessRules.fen2board(f); + } + + static get PIECES() + { + return ChessRules.PIECES.concat(['s','t','u','c','o']); + } setOtherVariables(fen) { @@ -65,7 +65,7 @@ export const VariantRules = class CheckeredRules extends ChessRules static IsGoodFen(fen) { - if (!ChessRules.IsGoodFen(fen)) + if (!ChessRules.IsGoodFen(fen)) return false; const fenParts = fen.split(" "); if (fenParts.length != 6) @@ -75,40 +75,40 @@ export const VariantRules = class CheckeredRules extends ChessRules return true; } - static IsGoodFlags(flags) - { - // 4 for castle + 16 for pawns - return !!flags.match(/^[01]{20,20}$/); - } - - setFlags(fenflags) - { - super.setFlags(fenflags); //castleFlags - this.pawnFlags = - { - "w": [...Array(8).fill(true)], //pawns can move 2 squares? - "b": [...Array(8).fill(true)], - }; - if (!fenflags) - return; - const flags = fenflags.substr(4); //skip first 4 digits, for castle - for (let c of ['w','b']) - { - for (let i=0; i<8; i++) - this.pawnFlags[c][i] = (flags.charAt((c=='w'?0:8)+i) == '1'); - } - } - - aggregateFlags() - { - return [this.castleFlags, this.pawnFlags]; - } - - disaggregateFlags(flags) - { - this.castleFlags = flags[0]; - this.pawnFlags = flags[1]; - } + static IsGoodFlags(flags) + { + // 4 for castle + 16 for pawns + return !!flags.match(/^[01]{20,20}$/); + } + + setFlags(fenflags) + { + super.setFlags(fenflags); //castleFlags + this.pawnFlags = + { + "w": [...Array(8).fill(true)], //pawns can move 2 squares? + "b": [...Array(8).fill(true)], + }; + if (!fenflags) + return; + const flags = fenflags.substr(4); //skip first 4 digits, for castle + for (let c of ['w','b']) + { + for (let i=0; i<8; i++) + this.pawnFlags[c][i] = (flags.charAt((c=='w'?0:8)+i) == '1'); + } + } + + aggregateFlags() + { + return [this.castleFlags, this.pawnFlags]; + } + + disaggregateFlags(flags) + { + this.castleFlags = flags[0]; + this.pawnFlags = flags[1]; + } getCmove(move) { @@ -117,171 +117,171 @@ export const VariantRules = class CheckeredRules extends ChessRules return null; } - canTake([x1,y1], [x2,y2]) - { - const color1 = this.getColor(x1,y1); - const color2 = this.getColor(x2,y2); - // Checkered aren't captured - return color1 != color2 && color2 != 'c' && (color1 != 'c' || color2 != this.turn); - } - - // Post-processing: apply "checkerization" of standard moves - getPotentialMovesFrom([x,y]) - { - let standardMoves = super.getPotentialMovesFrom([x,y]); - const lastRank = this.turn == "w" ? 0 : 7; - if (this.getPiece(x,y) == V.KING) - return standardMoves; //king has to be treated differently (for castles) - let moves = []; - standardMoves.forEach(m => { - if (m.vanish[0].p == V.PAWN) - { - if (Math.abs(m.end.x-m.start.x)==2 && !this.pawnFlags[this.turn][m.start.y]) - return; //skip forbidden 2-squares jumps - if (this.board[m.end.x][m.end.y] == V.EMPTY && m.vanish.length==2 - && this.getColor(m.start.x,m.start.y) == 'c') - { - return; //checkered pawns cannot take en-passant - } - } - if (m.vanish.length == 1) - moves.push(m); //no capture - else - { - // A capture occured (m.vanish.length == 2) - m.appear[0].c = "c"; - moves.push(m); - if (m.appear[0].p != m.vanish[1].p //avoid promotions (already treated): - && (m.vanish[0].p != V.PAWN || m.end.x != lastRank)) - { - // Add transformation into captured piece - let m2 = JSON.parse(JSON.stringify(m)); - m2.appear[0].p = m.vanish[1].p; - moves.push(m2); - } - } - }); - return moves; - } - - canIplay(side, [x,y]) - { - return (side == this.turn && [side,'c'].includes(this.getColor(x,y))); - } - - // Does m2 un-do m1 ? (to disallow undoing checkered moves) - oppositeMoves(m1, m2) - { - return (!!m1 && m2.appear[0].c == 'c' + canTake([x1,y1], [x2,y2]) + { + const color1 = this.getColor(x1,y1); + const color2 = this.getColor(x2,y2); + // Checkered aren't captured + return color1 != color2 && color2 != 'c' && (color1 != 'c' || color2 != this.turn); + } + + // Post-processing: apply "checkerization" of standard moves + getPotentialMovesFrom([x,y]) + { + let standardMoves = super.getPotentialMovesFrom([x,y]); + const lastRank = this.turn == "w" ? 0 : 7; + if (this.getPiece(x,y) == V.KING) + return standardMoves; //king has to be treated differently (for castles) + let moves = []; + standardMoves.forEach(m => { + if (m.vanish[0].p == V.PAWN) + { + if (Math.abs(m.end.x-m.start.x)==2 && !this.pawnFlags[this.turn][m.start.y]) + return; //skip forbidden 2-squares jumps + if (this.board[m.end.x][m.end.y] == V.EMPTY && m.vanish.length==2 + && this.getColor(m.start.x,m.start.y) == 'c') + { + return; //checkered pawns cannot take en-passant + } + } + if (m.vanish.length == 1) + moves.push(m); //no capture + else + { + // A capture occured (m.vanish.length == 2) + m.appear[0].c = "c"; + moves.push(m); + if (m.appear[0].p != m.vanish[1].p //avoid promotions (already treated): + && (m.vanish[0].p != V.PAWN || m.end.x != lastRank)) + { + // Add transformation into captured piece + let m2 = JSON.parse(JSON.stringify(m)); + m2.appear[0].p = m.vanish[1].p; + moves.push(m2); + } + } + }); + return moves; + } + + canIplay(side, [x,y]) + { + return (side == this.turn && [side,'c'].includes(this.getColor(x,y))); + } + + // Does m2 un-do m1 ? (to disallow undoing checkered moves) + oppositeMoves(m1, m2) + { + return (!!m1 && m2.appear[0].c == 'c' && m2.appear.length == 1 && m2.vanish.length == 1 - && m1.start.x == m2.end.x && m1.end.x == m2.start.x - && m1.start.y == m2.end.y && m1.end.y == m2.start.y); - } - - filterValid(moves) - { - if (moves.length == 0) - return []; - const color = this.turn; - return moves.filter(m => { - const L = this.cmoves.length; //at least 1: init from FEN - if (this.oppositeMoves(this.cmoves[L-1], m)) - return false; - this.play(m); - const res = !this.underCheck(color); - this.undo(m); - return res; - }); - } - - isAttackedByPawn([x,y], colors) - { - for (let c of colors) - { - const color = (c=="c" ? this.turn : c); - let pawnShift = (color=="w" ? 1 : -1); - if (x+pawnShift>=0 && x+pawnShift<8) - { - for (let i of [-1,1]) - { - if (y+i>=0 && y+i<8 && this.getPiece(x+pawnShift,y+i)==V.PAWN - && this.getColor(x+pawnShift,y+i)==c) - { - return true; - } - } - } - } - return false; - } - - underCheck(color) - { - return this.isAttacked(this.kingPos[color], [V.GetOppCol(color),'c']); - } - - getCheckSquares(color) - { - // Artifically change turn, for checkered pawns - this.turn = V.GetOppCol(color); - const kingAttacked = this.isAttacked( - this.kingPos[color], [V.GetOppCol(color),'c']); - let res = kingAttacked - ? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate! - : []; - this.turn = color; - return res; - } - - updateVariables(move) - { - super.updateVariables(move); - // Does this move turn off a 2-squares pawn flag? - const secondRank = [1,6]; - if (secondRank.includes(move.start.x) && move.vanish[0].p == V.PAWN) - this.pawnFlags[move.start.x==6 ? "w" : "b"][move.start.y] = false; - } - - getCurrentScore() - { + && m1.start.x == m2.end.x && m1.end.x == m2.start.x + && m1.start.y == m2.end.y && m1.end.y == m2.start.y); + } + + filterValid(moves) + { + if (moves.length == 0) + return []; + const color = this.turn; + return moves.filter(m => { + const L = this.cmoves.length; //at least 1: init from FEN + if (this.oppositeMoves(this.cmoves[L-1], m)) + return false; + this.play(m); + const res = !this.underCheck(color); + this.undo(m); + return res; + }); + } + + isAttackedByPawn([x,y], colors) + { + for (let c of colors) + { + const color = (c=="c" ? this.turn : c); + let pawnShift = (color=="w" ? 1 : -1); + if (x+pawnShift>=0 && x+pawnShift<8) + { + for (let i of [-1,1]) + { + if (y+i>=0 && y+i<8 && this.getPiece(x+pawnShift,y+i)==V.PAWN + && this.getColor(x+pawnShift,y+i)==c) + { + return true; + } + } + } + } + return false; + } + + underCheck(color) + { + return this.isAttacked(this.kingPos[color], [V.GetOppCol(color),'c']); + } + + getCheckSquares(color) + { + // Artifically change turn, for checkered pawns + this.turn = V.GetOppCol(color); + const kingAttacked = this.isAttacked( + this.kingPos[color], [V.GetOppCol(color),'c']); + let res = kingAttacked + ? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate! + : []; + this.turn = color; + return res; + } + + updateVariables(move) + { + super.updateVariables(move); + // Does this move turn off a 2-squares pawn flag? + const secondRank = [1,6]; + if (secondRank.includes(move.start.x) && move.vanish[0].p == V.PAWN) + this.pawnFlags[move.start.x==6 ? "w" : "b"][move.start.y] = false; + } + + getCurrentScore() + { if (this.atLeastOneMove()) // game not over return "*"; const color = this.turn; - // Artifically change turn, for checkered pawns - this.turn = V.GetOppCol(this.turn); - const res = this.isAttacked(this.kingPos[color], [V.GetOppCol(color),'c']) - ? (color == "w" ? "0-1" : "1-0") - : "1/2"; - this.turn = V.GetOppCol(this.turn); - return res; - } - - evalPosition() - { - let evaluation = 0; - //Just count material for now, considering checkered neutral (...) - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - { - if (this.board[i][j] != V.EMPTY) - { - const sqColor = this.getColor(i,j); - const sign = sqColor == "w" ? 1 : (sqColor=="b" ? -1 : 0); - evaluation += sign * V.VALUES[this.getPiece(i,j)]; - } - } - } - return evaluation; - } - - static GenRandInitFen() - { - const randFen = ChessRules.GenRandInitFen(); - // Add 16 pawns flags + empty cmove: - return randFen.replace(" w 0 1111", " w 0 11111111111111111111 -"); - } + // Artifically change turn, for checkered pawns + this.turn = V.GetOppCol(this.turn); + const res = this.isAttacked(this.kingPos[color], [V.GetOppCol(color),'c']) + ? (color == "w" ? "0-1" : "1-0") + : "1/2"; + this.turn = V.GetOppCol(this.turn); + return res; + } + + evalPosition() + { + let evaluation = 0; + //Just count material for now, considering checkered neutral (...) + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (this.board[i][j] != V.EMPTY) + { + const sqColor = this.getColor(i,j); + const sign = sqColor == "w" ? 1 : (sqColor=="b" ? -1 : 0); + evaluation += sign * V.VALUES[this.getPiece(i,j)]; + } + } + } + return evaluation; + } + + static GenRandInitFen() + { + const randFen = ChessRules.GenRandInitFen(); + // Add 16 pawns flags + empty cmove: + return randFen.replace(" w 0 1111", " w 0 11111111111111111111 -"); + } static ParseFen(fen) { @@ -301,17 +301,17 @@ export const VariantRules = class CheckeredRules extends ChessRules return super.getFen() + " " + cmoveFen; } - getFlagsFen() - { - let fen = super.getFlagsFen(); - // Add pawns flags - for (let c of ['w','b']) - { - for (let i=0; i<8; i++) - fen += this.pawnFlags[c][i] ? '1' : '0'; - } - return fen; - } + getFlagsFen() + { + let fen = super.getFlagsFen(); + // Add pawns flags + for (let c of ['w','b']) + { + for (let i=0; i<8; i++) + fen += this.pawnFlags[c][i] ? '1' : '0'; + } + return fen; + } // TODO (design): this cmove update here or in (un)updateVariables ? play(move) @@ -326,46 +326,46 @@ export const VariantRules = class CheckeredRules extends ChessRules super.undo(move); } - getNotation(move) - { - if (move.appear.length == 2) - { - // Castle - if (move.end.y < move.start.y) - return "0-0-0"; - else - return "0-0"; - } - - // Translate final square - const finalSquare = V.CoordsToSquare(move.end); - - const piece = this.getPiece(move.start.x, move.start.y); - if (piece == V.PAWN) - { - // Pawn move - let notation = ""; - if (move.vanish.length > 1) - { - // Capture - const startColumn = V.CoordToColumn(move.start.y); - notation = startColumn + "x" + finalSquare + - "=" + move.appear[0].p.toUpperCase(); - } - else //no capture - { - notation = finalSquare; - if (move.appear.length > 0 && piece != move.appear[0].p) //promotion - notation += "=" + move.appear[0].p.toUpperCase(); - } - return notation; - } - - else - { - // Piece movement - return piece.toUpperCase() + (move.vanish.length > 1 ? "x" : "") + finalSquare - + (move.vanish.length > 1 ? "=" + move.appear[0].p.toUpperCase() : ""); - } - } + getNotation(move) + { + if (move.appear.length == 2) + { + // Castle + if (move.end.y < move.start.y) + return "0-0-0"; + else + return "0-0"; + } + + // Translate final square + const finalSquare = V.CoordsToSquare(move.end); + + const piece = this.getPiece(move.start.x, move.start.y); + if (piece == V.PAWN) + { + // Pawn move + let notation = ""; + if (move.vanish.length > 1) + { + // Capture + const startColumn = V.CoordToColumn(move.start.y); + notation = startColumn + "x" + finalSquare + + "=" + move.appear[0].p.toUpperCase(); + } + else //no capture + { + notation = finalSquare; + if (move.appear.length > 0 && piece != move.appear[0].p) //promotion + notation += "=" + move.appear[0].p.toUpperCase(); + } + return notation; + } + + else + { + // Piece movement + return piece.toUpperCase() + (move.vanish.length > 1 ? "x" : "") + finalSquare + + (move.vanish.length > 1 ? "=" + move.appear[0].p.toUpperCase() : ""); + } + } } diff --git a/client/src/variants/Chess960.js b/client/src/variants/Chess960.js index 36397f69..74f4d1d5 100644 --- a/client/src/variants/Chess960.js +++ b/client/src/variants/Chess960.js @@ -1,5 +1,5 @@ import { ChessRules } from "@/base_rules"; export const VariantRules = class Chess960Rules extends ChessRules { - // Standard rules + // Standard rules } diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js index ec903c2f..82997274 100644 --- a/client/src/variants/Crazyhouse.js +++ b/client/src/variants/Crazyhouse.js @@ -3,284 +3,284 @@ import { ArrayFun} from "@/utils/array"; export const VariantRules = class CrazyhouseRules extends ChessRules { - static IsGoodFen(fen) - { - if (!ChessRules.IsGoodFen(fen)) - return false; - const fenParsed = V.ParseFen(fen); - // 5) Check reserves - if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{10,10}$/)) - return false; - // 6) Check promoted array - if (!fenParsed.promoted) - return false; - if (fenParsed.promoted == "-") - return true; //no promoted piece on board - const squares = fenParsed.promoted.split(","); - for (let square of squares) - { - const c = V.SquareToCoords(square); - if (c.y < 0 || c.y > V.size.y || isNaN(c.x) || c.x < 0 || c.x > V.size.x) - return false; - } - return true; - } + static IsGoodFen(fen) + { + if (!ChessRules.IsGoodFen(fen)) + return false; + const fenParsed = V.ParseFen(fen); + // 5) Check reserves + if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{10,10}$/)) + return false; + // 6) Check promoted array + if (!fenParsed.promoted) + return false; + if (fenParsed.promoted == "-") + return true; //no promoted piece on board + const squares = fenParsed.promoted.split(","); + for (let square of squares) + { + const c = V.SquareToCoords(square); + if (c.y < 0 || c.y > V.size.y || isNaN(c.x) || c.x < 0 || c.x > V.size.x) + return false; + } + return true; + } - static ParseFen(fen) - { - const fenParts = fen.split(" "); - return Object.assign( - ChessRules.ParseFen(fen), - { - reserve: fenParts[5], - promoted: fenParts[6], - } - ); - } + static ParseFen(fen) + { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { + reserve: fenParts[5], + promoted: fenParts[6], + } + ); + } - static GenRandInitFen() - { - return ChessRules.GenRandInitFen() + " 0000000000 -"; - } + static GenRandInitFen() + { + return ChessRules.GenRandInitFen() + " 0000000000 -"; + } - getFen() - { - return super.getFen() + " " + this.getReserveFen() + " " + this.getPromotedFen(); - } + getFen() + { + return super.getFen() + " " + this.getReserveFen() + " " + this.getPromotedFen(); + } - getReserveFen() - { - let counts = new Array(10); - for (let i=0; i<V.PIECES.length-1; i++) //-1: no king reserve - { - counts[i] = this.reserve["w"][V.PIECES[i]]; - counts[5+i] = this.reserve["b"][V.PIECES[i]]; - } - return counts.join(""); - } + getReserveFen() + { + let counts = new Array(10); + for (let i=0; i<V.PIECES.length-1; i++) //-1: no king reserve + { + counts[i] = this.reserve["w"][V.PIECES[i]]; + counts[5+i] = this.reserve["b"][V.PIECES[i]]; + } + return counts.join(""); + } - getPromotedFen() - { - let res = ""; - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - { - if (this.promoted[i][j]) - res += V.CoordsToSquare({x:i,y:j}); - } - } - if (res.length > 0) - res = res.slice(0,-1); //remove last comma - else - res = "-"; - return res; - } + getPromotedFen() + { + let res = ""; + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (this.promoted[i][j]) + res += V.CoordsToSquare({x:i,y:j}); + } + } + if (res.length > 0) + res = res.slice(0,-1); //remove last comma + else + res = "-"; + return res; + } - setOtherVariables(fen) - { - super.setOtherVariables(fen); - const fenParsed = V.ParseFen(fen); - // Also init reserves (used by the interface to show landable pieces) - this.reserve = - { - "w": - { - [V.PAWN]: parseInt(fenParsed.reserve[0]), - [V.ROOK]: parseInt(fenParsed.reserve[1]), - [V.KNIGHT]: parseInt(fenParsed.reserve[2]), - [V.BISHOP]: parseInt(fenParsed.reserve[3]), - [V.QUEEN]: parseInt(fenParsed.reserve[4]), - }, - "b": - { - [V.PAWN]: parseInt(fenParsed.reserve[5]), - [V.ROOK]: parseInt(fenParsed.reserve[6]), - [V.KNIGHT]: parseInt(fenParsed.reserve[7]), - [V.BISHOP]: parseInt(fenParsed.reserve[8]), - [V.QUEEN]: parseInt(fenParsed.reserve[9]), - } - }; - this.promoted = ArrayFun.init(V.size.x, V.size.y, false); - if (fenParsed.promoted != "-") - { - for (let square of fenParsed.promoted.split(",")) - { - const [x,y] = V.SquareToCoords(square); - promoted[x][y] = true; - } - } - } + setOtherVariables(fen) + { + super.setOtherVariables(fen); + const fenParsed = V.ParseFen(fen); + // Also init reserves (used by the interface to show landable pieces) + this.reserve = + { + "w": + { + [V.PAWN]: parseInt(fenParsed.reserve[0]), + [V.ROOK]: parseInt(fenParsed.reserve[1]), + [V.KNIGHT]: parseInt(fenParsed.reserve[2]), + [V.BISHOP]: parseInt(fenParsed.reserve[3]), + [V.QUEEN]: parseInt(fenParsed.reserve[4]), + }, + "b": + { + [V.PAWN]: parseInt(fenParsed.reserve[5]), + [V.ROOK]: parseInt(fenParsed.reserve[6]), + [V.KNIGHT]: parseInt(fenParsed.reserve[7]), + [V.BISHOP]: parseInt(fenParsed.reserve[8]), + [V.QUEEN]: parseInt(fenParsed.reserve[9]), + } + }; + this.promoted = ArrayFun.init(V.size.x, V.size.y, false); + if (fenParsed.promoted != "-") + { + for (let square of fenParsed.promoted.split(",")) + { + const [x,y] = V.SquareToCoords(square); + promoted[x][y] = true; + } + } + } - getColor(i,j) - { - if (i >= V.size.x) - return (i==V.size.x ? "w" : "b"); - return this.board[i][j].charAt(0); - } + getColor(i,j) + { + if (i >= V.size.x) + return (i==V.size.x ? "w" : "b"); + return this.board[i][j].charAt(0); + } - getPiece(i,j) - { - if (i >= V.size.x) - return V.RESERVE_PIECES[j]; - return this.board[i][j].charAt(1); - } + getPiece(i,j) + { + if (i >= V.size.x) + return V.RESERVE_PIECES[j]; + return this.board[i][j].charAt(1); + } - // Used by the interface: - getReservePpath(color, index) - { - return color + V.RESERVE_PIECES[index]; - } + // Used by the interface: + getReservePpath(color, index) + { + return color + V.RESERVE_PIECES[index]; + } - // Ordering on reserve pieces - static get RESERVE_PIECES() - { - return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]; - } + // Ordering on reserve pieces + static get RESERVE_PIECES() + { + return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]; + } - getReserveMoves([x,y]) - { - const color = this.turn; - const p = V.RESERVE_PIECES[y]; - if (this.reserve[color][p] == 0) - return []; - let moves = []; - const pawnShift = (p==V.PAWN ? 1 : 0); - for (let i=pawnShift; i<V.size.x-pawnShift; i++) - { - for (let j=0; j<V.size.y; j++) - { - if (this.board[i][j] == V.EMPTY) - { - let mv = new Move({ - appear: [ - new PiPo({ - x: i, - y: j, - c: color, - p: p - }) - ], - vanish: [], - start: {x:x, y:y}, //a bit artificial... - end: {x:i, y:j} - }); - moves.push(mv); - } - } - } - return moves; - } + getReserveMoves([x,y]) + { + const color = this.turn; + const p = V.RESERVE_PIECES[y]; + if (this.reserve[color][p] == 0) + return []; + let moves = []; + const pawnShift = (p==V.PAWN ? 1 : 0); + for (let i=pawnShift; i<V.size.x-pawnShift; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (this.board[i][j] == V.EMPTY) + { + let mv = new Move({ + appear: [ + new PiPo({ + x: i, + y: j, + c: color, + p: p + }) + ], + vanish: [], + start: {x:x, y:y}, //a bit artificial... + end: {x:i, y:j} + }); + moves.push(mv); + } + } + } + return moves; + } - getPotentialMovesFrom([x,y]) - { - if (x >= V.size.x) - { - // Reserves, outside of board: x == sizeX(+1) - return this.getReserveMoves([x,y]); - } - // Standard moves - return super.getPotentialMovesFrom([x,y]); - } + getPotentialMovesFrom([x,y]) + { + if (x >= V.size.x) + { + // Reserves, outside of board: x == sizeX(+1) + return this.getReserveMoves([x,y]); + } + // Standard moves + return super.getPotentialMovesFrom([x,y]); + } - getAllValidMoves() - { - let moves = super.getAllValidMoves(); - const color = this.turn; - for (let i=0; i<V.RESERVE_PIECES.length; i++) - moves = moves.concat(this.getReserveMoves([V.size.x+(color=="w"?0:1),i])); - return this.filterValid(moves); - } + getAllValidMoves() + { + let moves = super.getAllValidMoves(); + const color = this.turn; + for (let i=0; i<V.RESERVE_PIECES.length; i++) + moves = moves.concat(this.getReserveMoves([V.size.x+(color=="w"?0:1),i])); + return this.filterValid(moves); + } - atLeastOneMove() - { - if (!super.atLeastOneMove()) - { - const color = this.turn; - // Search one reserve move - for (let i=0; i<V.RESERVE_PIECES.length; i++) - { - let moves = this.filterValid( - this.getReserveMoves([V.size.x+(this.turn=="w"?0:1), i]) ); - if (moves.length > 0) - return true; - } - return false; - } - return true; - } + atLeastOneMove() + { + if (!super.atLeastOneMove()) + { + const color = this.turn; + // Search one reserve move + for (let i=0; i<V.RESERVE_PIECES.length; i++) + { + let moves = this.filterValid( + this.getReserveMoves([V.size.x+(this.turn=="w"?0:1), i]) ); + if (moves.length > 0) + return true; + } + return false; + } + return true; + } - updateVariables(move) - { - super.updateVariables(move); - if (move.vanish.length == 2 && move.appear.length == 2) - return; //skip castle - const color = move.appear[0].c; - if (move.vanish.length == 0) - { - this.reserve[color][move.appear[0].p]--; - return; - } - move.movePromoted = this.promoted[move.start.x][move.start.y]; - move.capturePromoted = this.promoted[move.end.x][move.end.y] - this.promoted[move.start.x][move.start.y] = false; - this.promoted[move.end.x][move.end.y] = move.movePromoted - || (move.vanish[0].p == V.PAWN && move.appear[0].p != V.PAWN); - if (move.capturePromoted) - this.reserve[color][V.PAWN]++; - else if (move.vanish.length == 2) - this.reserve[color][move.vanish[1].p]++; - } + updateVariables(move) + { + super.updateVariables(move); + if (move.vanish.length == 2 && move.appear.length == 2) + return; //skip castle + const color = move.appear[0].c; + if (move.vanish.length == 0) + { + this.reserve[color][move.appear[0].p]--; + return; + } + move.movePromoted = this.promoted[move.start.x][move.start.y]; + move.capturePromoted = this.promoted[move.end.x][move.end.y] + this.promoted[move.start.x][move.start.y] = false; + this.promoted[move.end.x][move.end.y] = move.movePromoted + || (move.vanish[0].p == V.PAWN && move.appear[0].p != V.PAWN); + if (move.capturePromoted) + this.reserve[color][V.PAWN]++; + else if (move.vanish.length == 2) + this.reserve[color][move.vanish[1].p]++; + } - unupdateVariables(move) - { - super.unupdateVariables(move); - if (move.vanish.length == 2 && move.appear.length == 2) - return; - const color = this.turn; - if (move.vanish.length == 0) - { - this.reserve[color][move.appear[0].p]++; - return; - } - if (move.movePromoted) - this.promoted[move.start.x][move.start.y] = true; - this.promoted[move.end.x][move.end.y] = move.capturePromoted; - if (move.capturePromoted) - this.reserve[color][V.PAWN]--; - else if (move.vanish.length == 2) - this.reserve[color][move.vanish[1].p]--; - } + unupdateVariables(move) + { + super.unupdateVariables(move); + if (move.vanish.length == 2 && move.appear.length == 2) + return; + const color = this.turn; + if (move.vanish.length == 0) + { + this.reserve[color][move.appear[0].p]++; + return; + } + if (move.movePromoted) + this.promoted[move.start.x][move.start.y] = true; + this.promoted[move.end.x][move.end.y] = move.capturePromoted; + if (move.capturePromoted) + this.reserve[color][V.PAWN]--; + else if (move.vanish.length == 2) + this.reserve[color][move.vanish[1].p]--; + } - static get SEARCH_DEPTH() { return 2; } //high branching factor + static get SEARCH_DEPTH() { return 2; } //high branching factor - evalPosition() - { - let evaluation = super.evalPosition(); - // Add reserves: - for (let i=0; i<V.RESERVE_PIECES.length; i++) - { - const p = V.RESERVE_PIECES[i]; - evaluation += this.reserve["w"][p] * V.VALUES[p]; - evaluation -= this.reserve["b"][p] * V.VALUES[p]; - } - return evaluation; - } + evalPosition() + { + let evaluation = super.evalPosition(); + // Add reserves: + for (let i=0; i<V.RESERVE_PIECES.length; i++) + { + const p = V.RESERVE_PIECES[i]; + evaluation += this.reserve["w"][p] * V.VALUES[p]; + evaluation -= this.reserve["b"][p] * V.VALUES[p]; + } + return evaluation; + } - getNotation(move) - { - if (move.vanish.length > 0) - return super.getNotation(move); - // Rebirth: - const piece = - (move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : ""); - return piece + "@" + V.CoordsToSquare(move.end); - } + getNotation(move) + { + if (move.vanish.length > 0) + return super.getNotation(move); + // Rebirth: + const piece = + (move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : ""); + return piece + "@" + V.CoordsToSquare(move.end); + } - getLongNotation(move) - { - if (move.vanish.length > 0) - return super.getLongNotation(move); - return "@" + V.CoordsToSquare(move.end); - } + getLongNotation(move) + { + if (move.vanish.length > 0) + return super.getLongNotation(move); + return "@" + V.CoordsToSquare(move.end); + } } diff --git a/client/src/variants/Dark.js b/client/src/variants/Dark.js index bc14cfa3..c0bfb09c 100644 --- a/client/src/variants/Dark.js +++ b/client/src/variants/Dark.js @@ -4,295 +4,295 @@ import { randInt } from "@/utils/alea"; export const VariantRules = class DarkRules extends ChessRules { - // Standard rules, in the shadow - setOtherVariables(fen) - { - super.setOtherVariables(fen); - const [sizeX,sizeY] = [V.size.x,V.size.y]; - this.enlightened = { - "w": ArrayFun.init(sizeX,sizeY), - "b": ArrayFun.init(sizeX,sizeY) - }; - // Setup enlightened: squares reachable by each side - // (TODO: one side would be enough ?) - this.updateEnlightened(); - } + // Standard rules, in the shadow + setOtherVariables(fen) + { + super.setOtherVariables(fen); + const [sizeX,sizeY] = [V.size.x,V.size.y]; + this.enlightened = { + "w": ArrayFun.init(sizeX,sizeY), + "b": ArrayFun.init(sizeX,sizeY) + }; + // Setup enlightened: squares reachable by each side + // (TODO: one side would be enough ?) + this.updateEnlightened(); + } - updateEnlightened() - { - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - { - this.enlightened["w"][i][j] = false; - this.enlightened["b"][i][j] = false; - } - } - const pawnShift = {"w":-1, "b":1}; - // Initialize with pieces positions (which are seen) - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - { - if (this.board[i][j] != V.EMPTY) - { - const color = this.getColor(i,j); - this.enlightened[color][i][j] = true; - // Add potential squares visible by "impossible pawn capture" - if (this.getPiece(i,j) == V.PAWN) - { - for (let shiftY of [-1,1]) - { - if (V.OnBoard(i+pawnShift[color],j+shiftY) - && this.board[i+pawnShift[color]][j+shiftY] == V.EMPTY) - { - this.enlightened[color][i+pawnShift[color]][j+shiftY] = true; - } - } - } - } - } - } - const currentTurn = this.turn; - this.turn = "w"; - const movesWhite = this.getAllValidMoves(); - this.turn = "b"; - const movesBlack = this.getAllValidMoves(); - this.turn = currentTurn; - for (let move of movesWhite) - this.enlightened["w"][move.end.x][move.end.y] = true; - for (let move of movesBlack) - this.enlightened["b"][move.end.x][move.end.y] = true; - } + updateEnlightened() + { + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + this.enlightened["w"][i][j] = false; + this.enlightened["b"][i][j] = false; + } + } + const pawnShift = {"w":-1, "b":1}; + // Initialize with pieces positions (which are seen) + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (this.board[i][j] != V.EMPTY) + { + const color = this.getColor(i,j); + this.enlightened[color][i][j] = true; + // Add potential squares visible by "impossible pawn capture" + if (this.getPiece(i,j) == V.PAWN) + { + for (let shiftY of [-1,1]) + { + if (V.OnBoard(i+pawnShift[color],j+shiftY) + && this.board[i+pawnShift[color]][j+shiftY] == V.EMPTY) + { + this.enlightened[color][i+pawnShift[color]][j+shiftY] = true; + } + } + } + } + } + } + const currentTurn = this.turn; + this.turn = "w"; + const movesWhite = this.getAllValidMoves(); + this.turn = "b"; + const movesBlack = this.getAllValidMoves(); + this.turn = currentTurn; + for (let move of movesWhite) + this.enlightened["w"][move.end.x][move.end.y] = true; + for (let move of movesBlack) + this.enlightened["b"][move.end.x][move.end.y] = true; + } - // Has to be redefined to avoid an infinite loop - getAllValidMoves() - { - const color = this.turn; - const oppCol = V.GetOppCol(color); - let potentialMoves = []; - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - { - if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == color) - Array.prototype.push.apply(potentialMoves, this.getPotentialMovesFrom([i,j])); - } - } - return potentialMoves; //because there are no checks - } + // Has to be redefined to avoid an infinite loop + getAllValidMoves() + { + const color = this.turn; + const oppCol = V.GetOppCol(color); + let potentialMoves = []; + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == color) + Array.prototype.push.apply(potentialMoves, this.getPotentialMovesFrom([i,j])); + } + } + return potentialMoves; //because there are no checks + } - atLeastOneMove() - { - if (this.kingPos[this.turn][0] < 0) - return false; - return true; //TODO: is it right? - } + atLeastOneMove() + { + if (this.kingPos[this.turn][0] < 0) + return false; + return true; //TODO: is it right? + } - underCheck(color) - { - return false; //there is no check - } + underCheck(color) + { + return false; //there is no check + } - getCheckSquares(color) - { - return []; - } + getCheckSquares(color) + { + return []; + } - updateVariables(move) - { - super.updateVariables(move); - if (move.vanish.length >= 2 && move.vanish[1].p == V.KING) - { - // We took opponent king ! (because if castle vanish[1] is a rook) - this.kingPos[this.turn] = [-1,-1]; - } + updateVariables(move) + { + super.updateVariables(move); + if (move.vanish.length >= 2 && move.vanish[1].p == V.KING) + { + // We took opponent king ! (because if castle vanish[1] is a rook) + this.kingPos[this.turn] = [-1,-1]; + } - // Update lights for both colors: - this.updateEnlightened(); - } + // Update lights for both colors: + this.updateEnlightened(); + } - unupdateVariables(move) - { - super.unupdateVariables(move); - const c = move.vanish[0].c; - const oppCol = V.GetOppCol(c); - if (this.kingPos[oppCol][0] < 0) - { - // Last move took opponent's king - for (let psq of move.vanish) - { - if (psq.p == 'k') - { - this.kingPos[oppCol] = [psq.x, psq.y]; - break; - } - } - } + unupdateVariables(move) + { + super.unupdateVariables(move); + const c = move.vanish[0].c; + const oppCol = V.GetOppCol(c); + if (this.kingPos[oppCol][0] < 0) + { + // Last move took opponent's king + for (let psq of move.vanish) + { + if (psq.p == 'k') + { + this.kingPos[oppCol] = [psq.x, psq.y]; + break; + } + } + } - // Update lights for both colors: - this.updateEnlightened(); - } + // Update lights for both colors: + this.updateEnlightened(); + } getCurrentScore() { - const color = this.turn; - const kp = this.kingPos[color]; - if (kp[0] < 0) //king disappeared - return (color == "w" ? "0-1" : "1-0"); + const color = this.turn; + const kp = this.kingPos[color]; + if (kp[0] < 0) //king disappeared + return (color == "w" ? "0-1" : "1-0"); if (this.atLeastOneMove()) // game not over return "*"; return "1/2"; //no moves but kings still there (seems impossible) - } + } - static get THRESHOLD_MATE() - { - return 500; //checkmates evals may be slightly below 1000 - } + static get THRESHOLD_MATE() + { + return 500; //checkmates evals may be slightly below 1000 + } - // In this special situation, we just look 1 half move ahead - getComputerMove() - { - const maxeval = V.INFINITY; - const color = this.turn; - const oppCol = V.GetOppCol(color); - const pawnShift = (color == "w" ? -1 : 1); + // In this special situation, we just look 1 half move ahead + getComputerMove() + { + const maxeval = V.INFINITY; + const color = this.turn; + const oppCol = V.GetOppCol(color); + const pawnShift = (color == "w" ? -1 : 1); - // Do not cheat: the current enlightment is all we can see - const myLight = JSON.parse(JSON.stringify(this.enlightened[color])); + // Do not cheat: the current enlightment is all we can see + const myLight = JSON.parse(JSON.stringify(this.enlightened[color])); - // Can a slider on (i,j) apparently take my king? - // NOTE: inaccurate because assume yes if some squares are shadowed - const sliderTake = ([i,j], piece) => { - const kp = this.kingPos[color]; - let step = undefined; - if (piece == V.BISHOP) - { - if (Math.abs(kp[0] - i) == Math.abs(kp[1] - j)) - { - step = - [ - (i-kp[0]) / Math.abs(i-kp[0]), - (j-kp[1]) / Math.abs(j-kp[1]) - ]; - } - } - else if (piece == V.ROOK) - { - if (kp[0] == i) - step = [0, (j-kp[1]) / Math.abs(j-kp[1])]; - else if (kp[1] == j) - step = [(i-kp[0]) / Math.abs(i-kp[0]), 0]; - } - if (!step) - return false; - // Check for obstacles - let obstacle = false; - for ( + // Can a slider on (i,j) apparently take my king? + // NOTE: inaccurate because assume yes if some squares are shadowed + const sliderTake = ([i,j], piece) => { + const kp = this.kingPos[color]; + let step = undefined; + if (piece == V.BISHOP) + { + if (Math.abs(kp[0] - i) == Math.abs(kp[1] - j)) + { + step = + [ + (i-kp[0]) / Math.abs(i-kp[0]), + (j-kp[1]) / Math.abs(j-kp[1]) + ]; + } + } + else if (piece == V.ROOK) + { + if (kp[0] == i) + step = [0, (j-kp[1]) / Math.abs(j-kp[1])]; + else if (kp[1] == j) + step = [(i-kp[0]) / Math.abs(i-kp[0]), 0]; + } + if (!step) + return false; + // Check for obstacles + let obstacle = false; + for ( let x=kp[0]+step[0], y=kp[1]+step[1]; - x != i && y != j; - x += step[0], y += step[1]) + x != i && y != j; + x += step[0], y += step[1]) { - if (myLight[x][y] && this.board[x][y] != V.EMPTY) - { - obstacle = true; - break; - } - } - if (!obstacle) - return true; - return false; - }; + if (myLight[x][y] && this.board[x][y] != V.EMPTY) + { + obstacle = true; + break; + } + } + if (!obstacle) + return true; + return false; + }; - // Do I see something which can take my king ? - const kingThreats = () => { - const kp = this.kingPos[color]; - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - { - if (myLight[i][j] && this.board[i][j] != V.EMPTY - && this.getColor(i,j) != color) - { - switch (this.getPiece(i,j)) - { - case V.PAWN: - if (kp[0] + pawnShift == i && Math.abs(kp[1]-j) == 1) - return true; - break; - case V.KNIGHT: - if ((Math.abs(kp[0] - i) == 2 && Math.abs(kp[1] - j) == 1) || - (Math.abs(kp[0] - i) == 1 && Math.abs(kp[1] - j) == 2)) - { - return true; - } - break; - case V.KING: - if (Math.abs(kp[0] - i) == 1 && Math.abs(kp[1] - j) == 1) - return true; - break; - case V.BISHOP: - if (sliderTake([i,j], V.BISHOP)) - return true; - break; - case V.ROOK: - if (sliderTake([i,j], V.ROOK)) - return true; - break; - case V.QUEEN: - if (sliderTake([i,j], V.BISHOP) || sliderTake([i,j], V.ROOK)) - return true; - break; - } - } - } - } - return false; - }; + // Do I see something which can take my king ? + const kingThreats = () => { + const kp = this.kingPos[color]; + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (myLight[i][j] && this.board[i][j] != V.EMPTY + && this.getColor(i,j) != color) + { + switch (this.getPiece(i,j)) + { + case V.PAWN: + if (kp[0] + pawnShift == i && Math.abs(kp[1]-j) == 1) + return true; + break; + case V.KNIGHT: + if ((Math.abs(kp[0] - i) == 2 && Math.abs(kp[1] - j) == 1) || + (Math.abs(kp[0] - i) == 1 && Math.abs(kp[1] - j) == 2)) + { + return true; + } + break; + case V.KING: + if (Math.abs(kp[0] - i) == 1 && Math.abs(kp[1] - j) == 1) + return true; + break; + case V.BISHOP: + if (sliderTake([i,j], V.BISHOP)) + return true; + break; + case V.ROOK: + if (sliderTake([i,j], V.ROOK)) + return true; + break; + case V.QUEEN: + if (sliderTake([i,j], V.BISHOP) || sliderTake([i,j], V.ROOK)) + return true; + break; + } + } + } + } + return false; + }; - let moves = this.getAllValidMoves(); - for (let move of moves) - { - this.play(move); - if (this.kingPos[oppCol][0] >= 0 && kingThreats()) - { - // We didn't take opponent king, and our king will be captured: bad - move.eval = -maxeval; - } - this.undo(move); + let moves = this.getAllValidMoves(); + for (let move of moves) + { + this.play(move); + if (this.kingPos[oppCol][0] >= 0 && kingThreats()) + { + // We didn't take opponent king, and our king will be captured: bad + move.eval = -maxeval; + } + this.undo(move); if (!!move.eval) - continue; + continue; - move.eval = 0; //a priori... + move.eval = 0; //a priori... - // Can I take something ? If yes, do it if it seems good... - if (move.vanish.length == 2 && move.vanish[1].c != color) //avoid castle - { - const myPieceVal = V.VALUES[move.appear[0].p]; - const hisPieceVal = V.VALUES[move.vanish[1].p]; - if (myPieceVal <= hisPieceVal) - move.eval = hisPieceVal - myPieceVal + 2; //favor captures - else - { - // Taking a pawn with minor piece, - // or minor piece or pawn with a rook, - // or anything but a queen with a queen, - // or anything with a king. - // ==> Do it at random, although - // this is clearly inferior to what a human can deduce... - move.eval = (Math.random() < 0.5 ? 1 : -1); - } - } - } + // Can I take something ? If yes, do it if it seems good... + if (move.vanish.length == 2 && move.vanish[1].c != color) //avoid castle + { + const myPieceVal = V.VALUES[move.appear[0].p]; + const hisPieceVal = V.VALUES[move.vanish[1].p]; + if (myPieceVal <= hisPieceVal) + move.eval = hisPieceVal - myPieceVal + 2; //favor captures + else + { + // Taking a pawn with minor piece, + // or minor piece or pawn with a rook, + // or anything but a queen with a queen, + // or anything with a king. + // ==> Do it at random, although + // this is clearly inferior to what a human can deduce... + move.eval = (Math.random() < 0.5 ? 1 : -1); + } + } + } - // TODO: also need to implement the case when an opponent piece (in light) - // is threatening something - maybe not the king, but e.g. pawn takes rook. + // TODO: also need to implement the case when an opponent piece (in light) + // is threatening something - maybe not the king, but e.g. pawn takes rook. - moves.sort((a,b) => b.eval - a.eval); - let candidates = [0]; - for (let j=1; j<moves.length && moves[j].eval == moves[0].eval; j++) - candidates.push(j); - return moves[candidates[randInt(candidates.length)]]; - } + moves.sort((a,b) => b.eval - a.eval); + let candidates = [0]; + for (let j=1; j<moves.length && moves[j].eval == moves[0].eval; j++) + candidates.push(j); + return moves[candidates[randInt(candidates.length)]]; + } } diff --git a/client/src/variants/Extinction.js b/client/src/variants/Extinction.js index 748f9e46..8d860aeb 100644 --- a/client/src/variants/Extinction.js +++ b/client/src/variants/Extinction.js @@ -2,130 +2,130 @@ import { ChessRules } from "@/base_rules"; export const VariantRules = class ExtinctionRules extends ChessRules { - setOtherVariables(fen) - { - super.setOtherVariables(fen); - const pos = V.ParseFen(fen).position; - // NOTE: no need for safety "|| []", because each piece type must be present - // (otherwise game is already over!) - this.material = - { - "w": - { - [V.KING]: pos.match(/K/g).length, - [V.QUEEN]: pos.match(/Q/g).length, - [V.ROOK]: pos.match(/R/g).length, - [V.KNIGHT]: pos.match(/N/g).length, - [V.BISHOP]: pos.match(/B/g).length, - [V.PAWN]: pos.match(/P/g).length - }, - "b": - { - [V.KING]: pos.match(/k/g).length, - [V.QUEEN]: pos.match(/q/g).length, - [V.ROOK]: pos.match(/r/g).length, - [V.KNIGHT]: pos.match(/n/g).length, - [V.BISHOP]: pos.match(/b/g).length, - [V.PAWN]: pos.match(/p/g).length - } - }; - } + setOtherVariables(fen) + { + super.setOtherVariables(fen); + const pos = V.ParseFen(fen).position; + // NOTE: no need for safety "|| []", because each piece type must be present + // (otherwise game is already over!) + this.material = + { + "w": + { + [V.KING]: pos.match(/K/g).length, + [V.QUEEN]: pos.match(/Q/g).length, + [V.ROOK]: pos.match(/R/g).length, + [V.KNIGHT]: pos.match(/N/g).length, + [V.BISHOP]: pos.match(/B/g).length, + [V.PAWN]: pos.match(/P/g).length + }, + "b": + { + [V.KING]: pos.match(/k/g).length, + [V.QUEEN]: pos.match(/q/g).length, + [V.ROOK]: pos.match(/r/g).length, + [V.KNIGHT]: pos.match(/n/g).length, + [V.BISHOP]: pos.match(/b/g).length, + [V.PAWN]: pos.match(/p/g).length + } + }; + } - getPotentialPawnMoves([x,y]) - { - let moves = super.getPotentialPawnMoves([x,y]); - // Add potential promotions into king - const color = this.turn; - const shift = (color == "w" ? -1 : 1); - const lastRank = (color == "w" ? 0 : V.size.x-1); + getPotentialPawnMoves([x,y]) + { + let moves = super.getPotentialPawnMoves([x,y]); + // Add potential promotions into king + const color = this.turn; + const shift = (color == "w" ? -1 : 1); + const lastRank = (color == "w" ? 0 : V.size.x-1); - if (x+shift == lastRank) - { - // Normal move - if (this.board[x+shift][y] == V.EMPTY) - moves.push(this.getBasicMove([x,y], [x+shift,y], {c:color,p:V.KING})); - // Captures - if (y>0 && this.board[x+shift][y-1] != V.EMPTY - && this.canTake([x,y], [x+shift,y-1])) - { - moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:color,p:V.KING})); - } - if (y<V.size.y-1 && this.board[x+shift][y+1] != V.EMPTY - && this.canTake([x,y], [x+shift,y+1])) - { - moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:color,p:V.KING})); - } - } + if (x+shift == lastRank) + { + // Normal move + if (this.board[x+shift][y] == V.EMPTY) + moves.push(this.getBasicMove([x,y], [x+shift,y], {c:color,p:V.KING})); + // Captures + if (y>0 && this.board[x+shift][y-1] != V.EMPTY + && this.canTake([x,y], [x+shift,y-1])) + { + moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:color,p:V.KING})); + } + if (y<V.size.y-1 && this.board[x+shift][y+1] != V.EMPTY + && this.canTake([x,y], [x+shift,y+1])) + { + moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:color,p:V.KING})); + } + } - return moves; - } + return moves; + } - // TODO: verify this assertion - atLeastOneMove() - { - return true; //always at least one possible move - } + // TODO: verify this assertion + atLeastOneMove() + { + return true; //always at least one possible move + } - underCheck(color) - { - return false; //there is no check - } + underCheck(color) + { + return false; //there is no check + } - getCheckSquares(color) - { - return []; - } + getCheckSquares(color) + { + return []; + } - updateVariables(move) - { - super.updateVariables(move); - // Treat the promotion case: (not the capture part) - if (move.appear[0].p != move.vanish[0].p) - { - this.material[move.appear[0].c][move.appear[0].p]++; - this.material[move.appear[0].c][V.PAWN]--; - } - if (move.vanish.length==2 && move.appear.length==1) //capture - this.material[move.vanish[1].c][move.vanish[1].p]--; - } + updateVariables(move) + { + super.updateVariables(move); + // Treat the promotion case: (not the capture part) + if (move.appear[0].p != move.vanish[0].p) + { + this.material[move.appear[0].c][move.appear[0].p]++; + this.material[move.appear[0].c][V.PAWN]--; + } + if (move.vanish.length==2 && move.appear.length==1) //capture + this.material[move.vanish[1].c][move.vanish[1].p]--; + } - unupdateVariables(move) - { - super.unupdateVariables(move); - if (move.appear[0].p != move.vanish[0].p) - { - this.material[move.appear[0].c][move.appear[0].p]--; - this.material[move.appear[0].c][V.PAWN]++; - } - if (move.vanish.length==2 && move.appear.length==1) - this.material[move.vanish[1].c][move.vanish[1].p]++; - } + unupdateVariables(move) + { + super.unupdateVariables(move); + if (move.appear[0].p != move.vanish[0].p) + { + this.material[move.appear[0].c][move.appear[0].p]--; + this.material[move.appear[0].c][V.PAWN]++; + } + if (move.vanish.length==2 && move.appear.length==1) + this.material[move.vanish[1].c][move.vanish[1].p]++; + } - getCurrentScore() - { - if (this.atLeastOneMove()) // game not over? - { - const color = this.turn; - if (Object.keys(this.material[color]).some( - p => { return this.material[color][p] == 0; })) - { - return (this.turn == "w" ? "0-1" : "1-0"); - } - return "*"; - } + getCurrentScore() + { + if (this.atLeastOneMove()) // game not over? + { + const color = this.turn; + if (Object.keys(this.material[color]).some( + p => { return this.material[color][p] == 0; })) + { + return (this.turn == "w" ? "0-1" : "1-0"); + } + return "*"; + } - return (this.turn == "w" ? "0-1" : "1-0"); //NOTE: currently unreachable... - } + return (this.turn == "w" ? "0-1" : "1-0"); //NOTE: currently unreachable... + } - evalPosition() - { - const color = this.turn; - if (Object.keys(this.material[color]).some( - p => { return this.material[color][p] == 0; })) - { - // Very negative (resp. positive) if white (reps. black) pieces set is incomplete - return (color=="w"?-1:1) * V.INFINITY; - } - return super.evalPosition(); - } + evalPosition() + { + const color = this.turn; + if (Object.keys(this.material[color]).some( + p => { return this.material[color][p] == 0; })) + { + // Very negative (resp. positive) if white (reps. black) pieces set is incomplete + return (color=="w"?-1:1) * V.INFINITY; + } + return super.evalPosition(); + } } diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js index 6ddfbd7c..a53f59e9 100644 --- a/client/src/variants/Grand.js +++ b/client/src/variants/Grand.js @@ -6,388 +6,388 @@ import { randInt } from "@/utils/alea"; // https://www.chessvariants.com/large.dir/freeling.html export const VariantRules = class GrandRules extends ChessRules { - static getPpath(b) - { - return ([V.MARSHALL,V.CARDINAL].includes(b[1]) ? "Grand/" : "") + b; - } - - static IsGoodFen(fen) - { - if (!ChessRules.IsGoodFen(fen)) - return false; - const fenParsed = V.ParseFen(fen); - // 5) Check captures - if (!fenParsed.captured || !fenParsed.captured.match(/^[0-9]{14,14}$/)) - return false; - return true; - } - - static IsGoodEnpassant(enpassant) - { - if (enpassant != "-") - { - const squares = enpassant.split(","); - if (squares.length > 2) - return false; - for (let sq of squares) - { - const ep = V.SquareToCoords(sq); - if (isNaN(ep.x) || !V.OnBoard(ep)) - return false; - } - } - return true; - } - - static ParseFen(fen) - { - const fenParts = fen.split(" "); - return Object.assign( - ChessRules.ParseFen(fen), - { captured: fenParts[5] } - ); - } - - getFen() - { - return super.getFen() + " " + this.getCapturedFen(); - } - - getCapturedFen() - { - let counts = [...Array(14).fill(0)]; - let i = 0; - for (let j=0; j<V.PIECES.length; j++) - { - if (V.PIECES[j] == V.KING) //no king captured - continue; - counts[i] = this.captured["w"][V.PIECES[i]]; - counts[7+i] = this.captured["b"][V.PIECES[i]]; - i++; - } - return counts.join(""); - } - - setOtherVariables(fen) - { - super.setOtherVariables(fen); - const fenParsed = V.ParseFen(fen); - // Initialize captured pieces' counts from FEN - this.captured = - { - "w": - { - [V.PAWN]: parseInt(fenParsed.captured[0]), - [V.ROOK]: parseInt(fenParsed.captured[1]), - [V.KNIGHT]: parseInt(fenParsed.captured[2]), - [V.BISHOP]: parseInt(fenParsed.captured[3]), - [V.QUEEN]: parseInt(fenParsed.captured[4]), - [V.MARSHALL]: parseInt(fenParsed.captured[5]), - [V.CARDINAL]: parseInt(fenParsed.captured[6]), - }, - "b": - { - [V.PAWN]: parseInt(fenParsed.captured[7]), - [V.ROOK]: parseInt(fenParsed.captured[8]), - [V.KNIGHT]: parseInt(fenParsed.captured[9]), - [V.BISHOP]: parseInt(fenParsed.captured[10]), - [V.QUEEN]: parseInt(fenParsed.captured[11]), - [V.MARSHALL]: parseInt(fenParsed.captured[12]), - [V.CARDINAL]: parseInt(fenParsed.captured[13]), - } - }; - } - - static get size() { return {x:10,y:10}; } - - static get MARSHALL() { return 'm'; } //rook+knight - static get CARDINAL() { return 'c'; } //bishop+knight - - static get PIECES() - { - return ChessRules.PIECES.concat([V.MARSHALL,V.CARDINAL]); - } - - // There may be 2 enPassant squares (if pawn jump 3 squares) - getEnpassantFen() - { - const L = this.epSquares.length; - if (!this.epSquares[L-1]) - return "-"; //no en-passant - let res = ""; - this.epSquares[L-1].forEach(sq => { - res += V.CoordsToSquare(sq) + ","; - }); - return res.slice(0,-1); //remove last comma - } - - // En-passant after 2-sq or 3-sq jumps - getEpSquare(moveOrSquare) - { - if (!moveOrSquare) - return undefined; - if (typeof moveOrSquare === "string") - { - const square = moveOrSquare; - if (square == "-") - return undefined; - let res = []; - square.split(",").forEach(sq => { - res.push(V.SquareToCoords(sq)); - }); - return res; - } - // Argument is a move: - const move = moveOrSquare; - const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; - if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) >= 2) - { - const step = (ex-sx) / Math.abs(ex-sx); - let res = [{ - x: sx + step, - y: sy - }]; - if (sx + 2*step != ex) //3-squares move - { - res.push({ - x: sx + 2*step, - y: sy - }); - } - return res; - } - return undefined; //default - } - - getPotentialMovesFrom([x,y]) - { - switch (this.getPiece(x,y)) - { - case V.MARSHALL: - return this.getPotentialMarshallMoves([x,y]); - case V.CARDINAL: - return this.getPotentialCardinalMoves([x,y]); - default: - return super.getPotentialMovesFrom([x,y]) - } - } - - // Special pawn rules: promotions to captured friendly pieces, - // optional on ranks 8-9 and mandatory on rank 10. - getPotentialPawnMoves([x,y]) - { - const color = this.turn; - let moves = []; - const [sizeX,sizeY] = [V.size.x,V.size.y]; - const shiftX = (color == "w" ? -1 : 1); - const startRanks = (color == "w" ? [sizeX-2,sizeX-3] : [1,2]); - const lastRanks = (color == "w" ? [0,1,2] : [sizeX-1,sizeX-2,sizeX-3]); - const promotionPieces = - [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.MARSHALL,V.CARDINAL]; - - // Always x+shiftX >= 0 && x+shiftX < sizeX, because no pawns on last rank - let finalPieces = undefined; - if (lastRanks.includes(x + shiftX)) - { - finalPieces = promotionPieces.filter(p => this.captured[color][p] > 0); - if (x + shiftX != lastRanks[0]) - finalPieces.push(V.PAWN); - } - else - finalPieces = [V.PAWN]; - if (this.board[x+shiftX][y] == V.EMPTY) - { - // One square forward - for (let piece of finalPieces) - moves.push(this.getBasicMove([x,y], [x+shiftX,y], {c:color,p:piece})); - if (startRanks.includes(x)) - { - if (this.board[x+2*shiftX][y] == V.EMPTY) - { - // Two squares jump - moves.push(this.getBasicMove([x,y], [x+2*shiftX,y])); - if (x==startRanks[0] && this.board[x+3*shiftX][y] == V.EMPTY) - { - // Three squares jump - moves.push(this.getBasicMove([x,y], [x+3*shiftX,y])); - } - } - } - } - // Captures - for (let shiftY of [-1,1]) - { - if (y + shiftY >= 0 && y + shiftY < sizeY - && this.board[x+shiftX][y+shiftY] != V.EMPTY - && this.canTake([x,y], [x+shiftX,y+shiftY])) - { - for (let piece of finalPieces) - { - moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], - {c:color,p:piece})); - } - } - } - - // En passant - const Lep = this.epSquares.length; - const epSquare = this.epSquares[Lep-1]; - if (!!epSquare) - { - for (let epsq of epSquare) - { - // TODO: some redundant checks - if (epsq.x == x+shiftX && Math.abs(epsq.y - y) == 1) - { - var enpassantMove = this.getBasicMove([x,y], [epsq.x,epsq.y]); - // WARNING: the captured pawn may be diagonally behind us, - // if it's a 3-squares jump and we take on 1st passing square - const px = (this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX); - enpassantMove.vanish.push({ - x: px, - y: epsq.y, - p: 'p', - c: this.getColor(px,epsq.y) - }); - moves.push(enpassantMove); - } - } - } - - return moves; - } - - // TODO: different castle? - - getPotentialMarshallMoves(sq) - { - return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat( - this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")); - } - - getPotentialCardinalMoves(sq) - { - return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat( - this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")); - } - - isAttacked(sq, colors) - { - return super.isAttacked(sq, colors) - || this.isAttackedByMarshall(sq, colors) - || this.isAttackedByCardinal(sq, colors); - } - - isAttackedByMarshall(sq, colors) - { - return this.isAttackedBySlideNJump(sq, colors, V.MARSHALL, V.steps[V.ROOK]) - || this.isAttackedBySlideNJump( - sq, colors, V.MARSHALL, V.steps[V.KNIGHT], "oneStep"); - } - - isAttackedByCardinal(sq, colors) - { - return this.isAttackedBySlideNJump(sq, colors, V.CARDINAL, V.steps[V.BISHOP]) - || this.isAttackedBySlideNJump( - sq, colors, V.CARDINAL, V.steps[V.KNIGHT], "oneStep"); - } - - updateVariables(move) - { - super.updateVariables(move); - if (move.vanish.length == 2 && move.appear.length == 1) - { - // Capture: update this.captured - this.captured[move.vanish[1].c][move.vanish[1].p]++; - } - if (move.vanish[0].p != move.appear[0].p) - { - // Promotion: update this.captured - this.captured[move.vanish[0].c][move.appear[0].p]--; - } - } - - unupdateVariables(move) - { - super.unupdateVariables(move); - if (move.vanish.length == 2 && move.appear.length == 1) - this.captured[move.vanish[1].c][move.vanish[1].p]--; - if (move.vanish[0].p != move.appear[0].p) - this.captured[move.vanish[0].c][move.appear[0].p]++; - } - - static get VALUES() - { - return Object.assign( - ChessRules.VALUES, - {'c': 5, 'm': 7} //experimental - ); - } - - static get SEARCH_DEPTH() { return 2; } - - // TODO: this function could be generalized and shared better (how ?!...) - static GenRandInitFen() - { - let pieces = { "w": new Array(10), "b": new Array(10) }; - // Shuffle pieces on first and last rank - for (let c of ["w","b"]) - { - let positions = ArrayFun.range(10); - - // Get random squares for bishops - let randIndex = 2 * randInt(5); - let bishop1Pos = positions[randIndex]; - // The second bishop must be on a square of different color - let randIndex_tmp = 2 * randInt(5) + 1; - let bishop2Pos = positions[randIndex_tmp]; - // Remove chosen squares - positions.splice(Math.max(randIndex,randIndex_tmp), 1); - positions.splice(Math.min(randIndex,randIndex_tmp), 1); - - // Get random squares for knights - randIndex = randInt(8); - let knight1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - randIndex = randInt(7); - let knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Get random square for queen - randIndex = randInt(6); - let queenPos = positions[randIndex]; - positions.splice(randIndex, 1); - - // ...random square for marshall - randIndex = randInt(5); - let marshallPos = positions[randIndex]; - positions.splice(randIndex, 1); - - // ...random square for cardinal - randIndex = randInt(4); - let cardinalPos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Rooks and king positions are now fixed, because of the ordering rook-king-rook - let rook1Pos = positions[0]; - let kingPos = positions[1]; - let rook2Pos = positions[2]; - - // Finally put the shuffled pieces in the board array - pieces[c][rook1Pos] = 'r'; - pieces[c][knight1Pos] = 'n'; - pieces[c][bishop1Pos] = 'b'; - pieces[c][queenPos] = 'q'; - pieces[c][marshallPos] = 'm'; - pieces[c][cardinalPos] = 'c'; - pieces[c][kingPos] = 'k'; - pieces[c][bishop2Pos] = 'b'; - pieces[c][knight2Pos] = 'n'; - pieces[c][rook2Pos] = 'r'; - } - return pieces["b"].join("") + - "/pppppppppp/10/10/10/10/10/10/PPPPPPPPPP/" + - pieces["w"].join("").toUpperCase() + - " w 0 1111 - 00000000000000"; - } + static getPpath(b) + { + return ([V.MARSHALL,V.CARDINAL].includes(b[1]) ? "Grand/" : "") + b; + } + + static IsGoodFen(fen) + { + if (!ChessRules.IsGoodFen(fen)) + return false; + const fenParsed = V.ParseFen(fen); + // 5) Check captures + if (!fenParsed.captured || !fenParsed.captured.match(/^[0-9]{14,14}$/)) + return false; + return true; + } + + static IsGoodEnpassant(enpassant) + { + if (enpassant != "-") + { + const squares = enpassant.split(","); + if (squares.length > 2) + return false; + for (let sq of squares) + { + const ep = V.SquareToCoords(sq); + if (isNaN(ep.x) || !V.OnBoard(ep)) + return false; + } + } + return true; + } + + static ParseFen(fen) + { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { captured: fenParts[5] } + ); + } + + getFen() + { + return super.getFen() + " " + this.getCapturedFen(); + } + + getCapturedFen() + { + let counts = [...Array(14).fill(0)]; + let i = 0; + for (let j=0; j<V.PIECES.length; j++) + { + if (V.PIECES[j] == V.KING) //no king captured + continue; + counts[i] = this.captured["w"][V.PIECES[i]]; + counts[7+i] = this.captured["b"][V.PIECES[i]]; + i++; + } + return counts.join(""); + } + + setOtherVariables(fen) + { + super.setOtherVariables(fen); + const fenParsed = V.ParseFen(fen); + // Initialize captured pieces' counts from FEN + this.captured = + { + "w": + { + [V.PAWN]: parseInt(fenParsed.captured[0]), + [V.ROOK]: parseInt(fenParsed.captured[1]), + [V.KNIGHT]: parseInt(fenParsed.captured[2]), + [V.BISHOP]: parseInt(fenParsed.captured[3]), + [V.QUEEN]: parseInt(fenParsed.captured[4]), + [V.MARSHALL]: parseInt(fenParsed.captured[5]), + [V.CARDINAL]: parseInt(fenParsed.captured[6]), + }, + "b": + { + [V.PAWN]: parseInt(fenParsed.captured[7]), + [V.ROOK]: parseInt(fenParsed.captured[8]), + [V.KNIGHT]: parseInt(fenParsed.captured[9]), + [V.BISHOP]: parseInt(fenParsed.captured[10]), + [V.QUEEN]: parseInt(fenParsed.captured[11]), + [V.MARSHALL]: parseInt(fenParsed.captured[12]), + [V.CARDINAL]: parseInt(fenParsed.captured[13]), + } + }; + } + + static get size() { return {x:10,y:10}; } + + static get MARSHALL() { return 'm'; } //rook+knight + static get CARDINAL() { return 'c'; } //bishop+knight + + static get PIECES() + { + return ChessRules.PIECES.concat([V.MARSHALL,V.CARDINAL]); + } + + // There may be 2 enPassant squares (if pawn jump 3 squares) + getEnpassantFen() + { + const L = this.epSquares.length; + if (!this.epSquares[L-1]) + return "-"; //no en-passant + let res = ""; + this.epSquares[L-1].forEach(sq => { + res += V.CoordsToSquare(sq) + ","; + }); + return res.slice(0,-1); //remove last comma + } + + // En-passant after 2-sq or 3-sq jumps + getEpSquare(moveOrSquare) + { + if (!moveOrSquare) + return undefined; + if (typeof moveOrSquare === "string") + { + const square = moveOrSquare; + if (square == "-") + return undefined; + let res = []; + square.split(",").forEach(sq => { + res.push(V.SquareToCoords(sq)); + }); + return res; + } + // Argument is a move: + const move = moveOrSquare; + const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; + if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) >= 2) + { + const step = (ex-sx) / Math.abs(ex-sx); + let res = [{ + x: sx + step, + y: sy + }]; + if (sx + 2*step != ex) //3-squares move + { + res.push({ + x: sx + 2*step, + y: sy + }); + } + return res; + } + return undefined; //default + } + + getPotentialMovesFrom([x,y]) + { + switch (this.getPiece(x,y)) + { + case V.MARSHALL: + return this.getPotentialMarshallMoves([x,y]); + case V.CARDINAL: + return this.getPotentialCardinalMoves([x,y]); + default: + return super.getPotentialMovesFrom([x,y]) + } + } + + // Special pawn rules: promotions to captured friendly pieces, + // optional on ranks 8-9 and mandatory on rank 10. + getPotentialPawnMoves([x,y]) + { + const color = this.turn; + let moves = []; + const [sizeX,sizeY] = [V.size.x,V.size.y]; + const shiftX = (color == "w" ? -1 : 1); + const startRanks = (color == "w" ? [sizeX-2,sizeX-3] : [1,2]); + const lastRanks = (color == "w" ? [0,1,2] : [sizeX-1,sizeX-2,sizeX-3]); + const promotionPieces = + [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.MARSHALL,V.CARDINAL]; + + // Always x+shiftX >= 0 && x+shiftX < sizeX, because no pawns on last rank + let finalPieces = undefined; + if (lastRanks.includes(x + shiftX)) + { + finalPieces = promotionPieces.filter(p => this.captured[color][p] > 0); + if (x + shiftX != lastRanks[0]) + finalPieces.push(V.PAWN); + } + else + finalPieces = [V.PAWN]; + if (this.board[x+shiftX][y] == V.EMPTY) + { + // One square forward + for (let piece of finalPieces) + moves.push(this.getBasicMove([x,y], [x+shiftX,y], {c:color,p:piece})); + if (startRanks.includes(x)) + { + if (this.board[x+2*shiftX][y] == V.EMPTY) + { + // Two squares jump + moves.push(this.getBasicMove([x,y], [x+2*shiftX,y])); + if (x==startRanks[0] && this.board[x+3*shiftX][y] == V.EMPTY) + { + // Three squares jump + moves.push(this.getBasicMove([x,y], [x+3*shiftX,y])); + } + } + } + } + // Captures + for (let shiftY of [-1,1]) + { + if (y + shiftY >= 0 && y + shiftY < sizeY + && this.board[x+shiftX][y+shiftY] != V.EMPTY + && this.canTake([x,y], [x+shiftX,y+shiftY])) + { + for (let piece of finalPieces) + { + moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], + {c:color,p:piece})); + } + } + } + + // En passant + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep-1]; + if (!!epSquare) + { + for (let epsq of epSquare) + { + // TODO: some redundant checks + if (epsq.x == x+shiftX && Math.abs(epsq.y - y) == 1) + { + var enpassantMove = this.getBasicMove([x,y], [epsq.x,epsq.y]); + // WARNING: the captured pawn may be diagonally behind us, + // if it's a 3-squares jump and we take on 1st passing square + const px = (this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX); + enpassantMove.vanish.push({ + x: px, + y: epsq.y, + p: 'p', + c: this.getColor(px,epsq.y) + }); + moves.push(enpassantMove); + } + } + } + + return moves; + } + + // TODO: different castle? + + getPotentialMarshallMoves(sq) + { + return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat( + this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")); + } + + getPotentialCardinalMoves(sq) + { + return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat( + this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")); + } + + isAttacked(sq, colors) + { + return super.isAttacked(sq, colors) + || this.isAttackedByMarshall(sq, colors) + || this.isAttackedByCardinal(sq, colors); + } + + isAttackedByMarshall(sq, colors) + { + return this.isAttackedBySlideNJump(sq, colors, V.MARSHALL, V.steps[V.ROOK]) + || this.isAttackedBySlideNJump( + sq, colors, V.MARSHALL, V.steps[V.KNIGHT], "oneStep"); + } + + isAttackedByCardinal(sq, colors) + { + return this.isAttackedBySlideNJump(sq, colors, V.CARDINAL, V.steps[V.BISHOP]) + || this.isAttackedBySlideNJump( + sq, colors, V.CARDINAL, V.steps[V.KNIGHT], "oneStep"); + } + + updateVariables(move) + { + super.updateVariables(move); + if (move.vanish.length == 2 && move.appear.length == 1) + { + // Capture: update this.captured + this.captured[move.vanish[1].c][move.vanish[1].p]++; + } + if (move.vanish[0].p != move.appear[0].p) + { + // Promotion: update this.captured + this.captured[move.vanish[0].c][move.appear[0].p]--; + } + } + + unupdateVariables(move) + { + super.unupdateVariables(move); + if (move.vanish.length == 2 && move.appear.length == 1) + this.captured[move.vanish[1].c][move.vanish[1].p]--; + if (move.vanish[0].p != move.appear[0].p) + this.captured[move.vanish[0].c][move.appear[0].p]++; + } + + static get VALUES() + { + return Object.assign( + ChessRules.VALUES, + {'c': 5, 'm': 7} //experimental + ); + } + + static get SEARCH_DEPTH() { return 2; } + + // TODO: this function could be generalized and shared better (how ?!...) + static GenRandInitFen() + { + let pieces = { "w": new Array(10), "b": new Array(10) }; + // Shuffle pieces on first and last rank + for (let c of ["w","b"]) + { + let positions = ArrayFun.range(10); + + // Get random squares for bishops + let randIndex = 2 * randInt(5); + let bishop1Pos = positions[randIndex]; + // The second bishop must be on a square of different color + let randIndex_tmp = 2 * randInt(5) + 1; + let bishop2Pos = positions[randIndex_tmp]; + // Remove chosen squares + positions.splice(Math.max(randIndex,randIndex_tmp), 1); + positions.splice(Math.min(randIndex,randIndex_tmp), 1); + + // Get random squares for knights + randIndex = randInt(8); + let knight1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + randIndex = randInt(7); + let knight2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Get random square for queen + randIndex = randInt(6); + let queenPos = positions[randIndex]; + positions.splice(randIndex, 1); + + // ...random square for marshall + randIndex = randInt(5); + let marshallPos = positions[randIndex]; + positions.splice(randIndex, 1); + + // ...random square for cardinal + randIndex = randInt(4); + let cardinalPos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Rooks and king positions are now fixed, because of the ordering rook-king-rook + let rook1Pos = positions[0]; + let kingPos = positions[1]; + let rook2Pos = positions[2]; + + // Finally put the shuffled pieces in the board array + pieces[c][rook1Pos] = 'r'; + pieces[c][knight1Pos] = 'n'; + pieces[c][bishop1Pos] = 'b'; + pieces[c][queenPos] = 'q'; + pieces[c][marshallPos] = 'm'; + pieces[c][cardinalPos] = 'c'; + pieces[c][kingPos] = 'k'; + pieces[c][bishop2Pos] = 'b'; + pieces[c][knight2Pos] = 'n'; + pieces[c][rook2Pos] = 'r'; + } + return pieces["b"].join("") + + "/pppppppppp/10/10/10/10/10/10/PPPPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + " w 0 1111 - 00000000000000"; + } } diff --git a/client/src/variants/Losers.js b/client/src/variants/Losers.js index 86b00f90..d815ee7a 100644 --- a/client/src/variants/Losers.js +++ b/client/src/variants/Losers.js @@ -4,106 +4,106 @@ import { randInt } from "@/utils/alea"; export const VariantRules = class LosersRules extends ChessRules { - static get HasFlags() { return false; } - - getPotentialPawnMoves([x,y]) - { - let moves = super.getPotentialPawnMoves([x,y]); - - // Complete with promotion(s) into king, if possible - const color = this.turn; - const shift = (color == "w" ? -1 : 1); - const lastRank = (color == "w" ? 0 : V.size.x-1); - if (x+shift == lastRank) - { - // Normal move - if (this.board[x+shift][y] == V.EMPTY) - moves.push(this.getBasicMove([x,y], [x+shift,y], {c:color,p:V.KING})); - // Captures - if (y>0 && this.canTake([x,y], [x+shift,y-1]) - && this.board[x+shift][y-1] != V.EMPTY) - { - moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:color,p:V.KING})); - } - if (y<V.size.y-1 && this.canTake([x,y], [x+shift,y+1]) - && this.board[x+shift][y+1] != V.EMPTY) - { - moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:color,p:V.KING})); - } - } - - return moves; - } - - getPotentialKingMoves(sq) - { - // No castle: - return this.getSlideNJumpMoves(sq, - V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); - } - - // Stop at the first capture found (if any) - atLeastOneCapture() - { - const color = this.turn; - const oppCol = V.GetOppCol(color); - for (let i=0; i<V.size.x; i++) - { - for (let j=0; j<V.size.y; j++) - { - if (this.board[i][j] != V.EMPTY && this.getColor(i,j) != oppCol) - { - const moves = this.getPotentialMovesFrom([i,j]); - if (moves.length > 0) - { - for (let k=0; k<moves.length; k++) - { - if (moves[k].vanish.length==2 && this.filterValid([moves[k]]).length > 0) - return true; - } - } - } - } - } - return false; - } - - // Trim all non-capturing moves - static KeepCaptures(moves) - { - return moves.filter(m => { return m.vanish.length == 2; }); - } - - getPossibleMovesFrom(sq) - { - let moves = this.filterValid( this.getPotentialMovesFrom(sq) ); - // This is called from interface: we need to know if a capture is possible - if (this.atLeastOneCapture()) - moves = V.KeepCaptures(moves); - return moves; - } - - getAllValidMoves() - { - let moves = super.getAllValidMoves(); - if (moves.some(m => { return m.vanish.length == 2; })) - moves = V.KeepCaptures(moves); - return moves; - } - - underCheck(color) - { - return false; //No notion of check - } - - getCheckSquares(move) - { - return []; - } - - // No variables update because no royal king + no castling - updateVariables(move) { } - unupdateVariables(move) { } + static get HasFlags() { return false; } + + getPotentialPawnMoves([x,y]) + { + let moves = super.getPotentialPawnMoves([x,y]); + + // Complete with promotion(s) into king, if possible + const color = this.turn; + const shift = (color == "w" ? -1 : 1); + const lastRank = (color == "w" ? 0 : V.size.x-1); + if (x+shift == lastRank) + { + // Normal move + if (this.board[x+shift][y] == V.EMPTY) + moves.push(this.getBasicMove([x,y], [x+shift,y], {c:color,p:V.KING})); + // Captures + if (y>0 && this.canTake([x,y], [x+shift,y-1]) + && this.board[x+shift][y-1] != V.EMPTY) + { + moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:color,p:V.KING})); + } + if (y<V.size.y-1 && this.canTake([x,y], [x+shift,y+1]) + && this.board[x+shift][y+1] != V.EMPTY) + { + moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:color,p:V.KING})); + } + } + + return moves; + } + + getPotentialKingMoves(sq) + { + // No castle: + return this.getSlideNJumpMoves(sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + } + + // Stop at the first capture found (if any) + atLeastOneCapture() + { + const color = this.turn; + const oppCol = V.GetOppCol(color); + for (let i=0; i<V.size.x; i++) + { + for (let j=0; j<V.size.y; j++) + { + if (this.board[i][j] != V.EMPTY && this.getColor(i,j) != oppCol) + { + const moves = this.getPotentialMovesFrom([i,j]); + if (moves.length > 0) + { + for (let k=0; k<moves.length; k++) + { + if (moves[k].vanish.length==2 && this.filterValid([moves[k]]).length > 0) + return true; + } + } + } + } + } + return false; + } + + // Trim all non-capturing moves + static KeepCaptures(moves) + { + return moves.filter(m => { return m.vanish.length == 2; }); + } + + getPossibleMovesFrom(sq) + { + let moves = this.filterValid( this.getPotentialMovesFrom(sq) ); + // This is called from interface: we need to know if a capture is possible + if (this.atLeastOneCapture()) + moves = V.KeepCaptures(moves); + return moves; + } + + getAllValidMoves() + { + let moves = super.getAllValidMoves(); + if (moves.some(m => { return m.vanish.length == 2; })) + moves = V.KeepCaptures(moves); + return moves; + } + + underCheck(color) + { + return false; //No notion of check + } + + getCheckSquares(move) + { + return []; + } + + // No variables update because no royal king + no castling + updateVariables(move) { } + unupdateVariables(move) { } getCurrentScore() { @@ -111,82 +111,82 @@ export const VariantRules = class LosersRules extends ChessRules return "*"; // No valid move: the side who cannot move wins - return (this.turn == "w" ? "1-0" : "0-1"); - } - - static get VALUES() - { - // Experimental... - return { - 'p': 1, - 'r': 7, - 'n': 3, - 'b': 3, - 'q': 5, - 'k': 5 - }; - } - - static get SEARCH_DEPTH() { return 4; } - - evalPosition() - { - return - super.evalPosition(); //better with less material - } - - static GenRandInitFen() - { - let pieces = { "w": new Array(8), "b": new Array(8) }; - // Shuffle pieces on first and last rank - for (let c of ["w","b"]) - { - let positions = ArrayFun.range(8); - - // Get random squares for bishops - let randIndex = 2 * randInt(4); - let bishop1Pos = positions[randIndex]; - // The second bishop must be on a square of different color - let randIndex_tmp = 2 * randInt(4) + 1; - let bishop2Pos = positions[randIndex_tmp]; - // Remove chosen squares - positions.splice(Math.max(randIndex,randIndex_tmp), 1); - positions.splice(Math.min(randIndex,randIndex_tmp), 1); - - // Get random squares for knights - randIndex = randInt(6); - let knight1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - randIndex = randInt(5); - let knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Get random square for queen - randIndex = randInt(4); - let queenPos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Random square for king (no castle) - randIndex = randInt(3); - let kingPos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Rooks positions are now fixed - let rook1Pos = positions[0]; - let rook2Pos = positions[1]; - - // Finally put the shuffled pieces in the board array - pieces[c][rook1Pos] = 'r'; - pieces[c][knight1Pos] = 'n'; - pieces[c][bishop1Pos] = 'b'; - pieces[c][queenPos] = 'q'; - pieces[c][kingPos] = 'k'; - pieces[c][bishop2Pos] = 'b'; - pieces[c][knight2Pos] = 'n'; - pieces[c][rook2Pos] = 'r'; - } - return pieces["b"].join("") + - "/pppppppp/8/8/8/8/PPPPPPPP/" + - pieces["w"].join("").toUpperCase() + - " w 0 -"; //en-passant allowed, but no flags - } + return (this.turn == "w" ? "1-0" : "0-1"); + } + + static get VALUES() + { + // Experimental... + return { + 'p': 1, + 'r': 7, + 'n': 3, + 'b': 3, + 'q': 5, + 'k': 5 + }; + } + + static get SEARCH_DEPTH() { return 4; } + + evalPosition() + { + return - super.evalPosition(); //better with less material + } + + static GenRandInitFen() + { + let pieces = { "w": new Array(8), "b": new Array(8) }; + // Shuffle pieces on first and last rank + for (let c of ["w","b"]) + { + let positions = ArrayFun.range(8); + + // Get random squares for bishops + let randIndex = 2 * randInt(4); + let bishop1Pos = positions[randIndex]; + // The second bishop must be on a square of different color + let randIndex_tmp = 2 * randInt(4) + 1; + let bishop2Pos = positions[randIndex_tmp]; + // Remove chosen squares + positions.splice(Math.max(randIndex,randIndex_tmp), 1); + positions.splice(Math.min(randIndex,randIndex_tmp), 1); + + // Get random squares for knights + randIndex = randInt(6); + let knight1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + randIndex = randInt(5); + let knight2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Get random square for queen + randIndex = randInt(4); + let queenPos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Random square for king (no castle) + randIndex = randInt(3); + let kingPos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Rooks positions are now fixed + let rook1Pos = positions[0]; + let rook2Pos = positions[1]; + + // Finally put the shuffled pieces in the board array + pieces[c][rook1Pos] = 'r'; + pieces[c][knight1Pos] = 'n'; + pieces[c][bishop1Pos] = 'b'; + pieces[c][queenPos] = 'q'; + pieces[c][kingPos] = 'k'; + pieces[c][bishop2Pos] = 'b'; + pieces[c][knight2Pos] = 'n'; + pieces[c][rook2Pos] = 'r'; + } + return pieces["b"].join("") + + "/pppppppp/8/8/8/8/PPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + " w 0 -"; //en-passant allowed, but no flags + } } diff --git a/client/src/variants/Magnetic.js b/client/src/variants/Magnetic.js index ea745dd6..3789de38 100644 --- a/client/src/variants/Magnetic.js +++ b/client/src/variants/Magnetic.js @@ -2,210 +2,210 @@ import { ChessRules, PiPo } from "@/base_rules"; export const VariantRules = class MagneticRules extends ChessRules { - static get HasEnpassant() { return false; } + static get HasEnpassant() { return false; } - getPotentialMovesFrom([x,y]) - { - let standardMoves = super.getPotentialMovesFrom([x,y]); - let moves = []; - standardMoves.forEach(m => { - let newMove_s = this.applyMagneticLaws(m); - if (newMove_s.length == 1) - moves.push(newMove_s[0]); - else //promotion - moves = moves.concat(newMove_s); - }); - return moves; - } + getPotentialMovesFrom([x,y]) + { + let standardMoves = super.getPotentialMovesFrom([x,y]); + let moves = []; + standardMoves.forEach(m => { + let newMove_s = this.applyMagneticLaws(m); + if (newMove_s.length == 1) + moves.push(newMove_s[0]); + else //promotion + moves = moves.concat(newMove_s); + }); + return moves; + } - // Complete a move with magnetic actions - // TODO: job is done multiple times for (normal) promotions. - applyMagneticLaws(move) - { - if (move.appear[0].p == V.KING && move.appear.length==1) - return [move]; //kings are not charged - const aIdx = (move.appear[0].p != V.KING ? 0 : 1); //if castling, rook is charged - const [x,y] = [move.appear[aIdx].x, move.appear[aIdx].y]; - const color = this.turn; - const lastRank = (color=="w" ? 0 : 7); - const standardMove = JSON.parse(JSON.stringify(move)); - this.play(standardMove); - for (let step of [[-1,0],[1,0],[0,-1],[0,1]]) - { - let [i,j] = [x+step[0],y+step[1]]; - while (V.OnBoard(i,j)) - { - if (this.board[i][j] != V.EMPTY) - { - // Found something. Same color or not? - if (this.getColor(i,j) != color) - { - // Attraction - if ((Math.abs(i-x)>=2 || Math.abs(j-y)>=2) && this.getPiece(i,j) != V.KING) - { - move.vanish.push( - new PiPo({ - p:this.getPiece(i,j), - c:this.getColor(i,j), - x:i, - y:j - }) - ); - move.appear.push( - new PiPo({ - p:this.getPiece(i,j), - c:this.getColor(i,j), - x:x+step[0], - y:y+step[1] - }) - ); - } - } - else - { - // Repulsion - if (this.getPiece(i,j) != V.KING) - { - // Push it until we meet an obstacle or edge of the board - let [ii,jj] = [i+step[0],j+step[1]]; - while (V.OnBoard(ii,jj)) - { - if (this.board[ii][jj] != V.EMPTY) - break; - ii += step[0]; - jj += step[1]; - } - ii -= step[0]; - jj -= step[1]; - if (Math.abs(ii-i)>=1 || Math.abs(jj-j)>=1) - { - move.vanish.push( - new PiPo({ - p:this.getPiece(i,j), - c:this.getColor(i,j), - x:i, - y:j - }) - ); - move.appear.push( - new PiPo({ - p:this.getPiece(i,j), - c:this.getColor(i,j), - x:ii, - y:jj - }) - ); - } - } - } - break; - } - i += step[0]; - j += step[1]; - } - } - this.undo(standardMove); - let moves = []; - // Scan move for pawn (max 1) on 8th rank - for (let i=1; i<move.appear.length; i++) - { - if (move.appear[i].p==V.PAWN && move.appear[i].c==color - && move.appear[i].x==lastRank) - { - move.appear[i].p = V.ROOK; - moves.push(move); - for (let piece of [V.KNIGHT, V.BISHOP, V.QUEEN]) - { - let cmove = JSON.parse(JSON.stringify(move)); - cmove.appear[i].p = piece; - moves.push(cmove); - } - // Swap appear[i] and appear[0] for moves presentation (TODO: this is awkward) - moves.forEach(m => { - let tmp = m.appear[0]; - m.appear[0] = m.appear[i]; - m.appear[i] = tmp; - }); - break; - } - } - if (moves.length == 0) //no pawn on 8th rank - moves.push(move); - return moves; - } + // Complete a move with magnetic actions + // TODO: job is done multiple times for (normal) promotions. + applyMagneticLaws(move) + { + if (move.appear[0].p == V.KING && move.appear.length==1) + return [move]; //kings are not charged + const aIdx = (move.appear[0].p != V.KING ? 0 : 1); //if castling, rook is charged + const [x,y] = [move.appear[aIdx].x, move.appear[aIdx].y]; + const color = this.turn; + const lastRank = (color=="w" ? 0 : 7); + const standardMove = JSON.parse(JSON.stringify(move)); + this.play(standardMove); + for (let step of [[-1,0],[1,0],[0,-1],[0,1]]) + { + let [i,j] = [x+step[0],y+step[1]]; + while (V.OnBoard(i,j)) + { + if (this.board[i][j] != V.EMPTY) + { + // Found something. Same color or not? + if (this.getColor(i,j) != color) + { + // Attraction + if ((Math.abs(i-x)>=2 || Math.abs(j-y)>=2) && this.getPiece(i,j) != V.KING) + { + move.vanish.push( + new PiPo({ + p:this.getPiece(i,j), + c:this.getColor(i,j), + x:i, + y:j + }) + ); + move.appear.push( + new PiPo({ + p:this.getPiece(i,j), + c:this.getColor(i,j), + x:x+step[0], + y:y+step[1] + }) + ); + } + } + else + { + // Repulsion + if (this.getPiece(i,j) != V.KING) + { + // Push it until we meet an obstacle or edge of the board + let [ii,jj] = [i+step[0],j+step[1]]; + while (V.OnBoard(ii,jj)) + { + if (this.board[ii][jj] != V.EMPTY) + break; + ii += step[0]; + jj += step[1]; + } + ii -= step[0]; + jj -= step[1]; + if (Math.abs(ii-i)>=1 || Math.abs(jj-j)>=1) + { + move.vanish.push( + new PiPo({ + p:this.getPiece(i,j), + c:this.getColor(i,j), + x:i, + y:j + }) + ); + move.appear.push( + new PiPo({ + p:this.getPiece(i,j), + c:this.getColor(i,j), + x:ii, + y:jj + }) + ); + } + } + } + break; + } + i += step[0]; + j += step[1]; + } + } + this.undo(standardMove); + let moves = []; + // Scan move for pawn (max 1) on 8th rank + for (let i=1; i<move.appear.length; i++) + { + if (move.appear[i].p==V.PAWN && move.appear[i].c==color + && move.appear[i].x==lastRank) + { + move.appear[i].p = V.ROOK; + moves.push(move); + for (let piece of [V.KNIGHT, V.BISHOP, V.QUEEN]) + { + let cmove = JSON.parse(JSON.stringify(move)); + cmove.appear[i].p = piece; + moves.push(cmove); + } + // Swap appear[i] and appear[0] for moves presentation (TODO: this is awkward) + moves.forEach(m => { + let tmp = m.appear[0]; + m.appear[0] = m.appear[i]; + m.appear[i] = tmp; + }); + break; + } + } + if (moves.length == 0) //no pawn on 8th rank + moves.push(move); + return moves; + } - atLeastOneMove() - { - if (this.kingPos[this.turn][0] < 0) - return false; - return true; //TODO: is it right? - } + atLeastOneMove() + { + if (this.kingPos[this.turn][0] < 0) + return false; + return true; //TODO: is it right? + } - underCheck(color) - { - return false; //there is no check - } + underCheck(color) + { + return false; //there is no check + } - getCheckSquares(move) - { - return []; - } + getCheckSquares(move) + { + return []; + } - updateVariables(move) - { - super.updateVariables(move); - const c = move.vanish[0].c; - if (move.vanish.length >= 2 && move.vanish[1].p == V.KING) - { - // We took opponent king ! - const oppCol = V.GetOppCol(c); - this.kingPos[oppCol] = [-1,-1]; - this.castleFlags[oppCol] = [false,false]; - } - // Did we magnetically move our (init) rooks or opponents' ones ? - const firstRank = (c == "w" ? 7 : 0); - const oppFirstRank = 7 - firstRank; - const oppCol = V.GetOppCol(c); - move.vanish.forEach(psq => { - if (psq.x == firstRank && this.INIT_COL_ROOK[c].includes(psq.y)) - this.castleFlags[c][psq.y==this.INIT_COL_ROOK[c][0] ? 0 : 1] = false; - else if (psq.x == oppFirstRank && this.INIT_COL_ROOK[oppCol].includes(psq.y)) - this.castleFlags[oppCol][psq.y==this.INIT_COL_ROOK[oppCol][0] ? 0 : 1] = false; - }); - } + updateVariables(move) + { + super.updateVariables(move); + const c = move.vanish[0].c; + if (move.vanish.length >= 2 && move.vanish[1].p == V.KING) + { + // We took opponent king ! + const oppCol = V.GetOppCol(c); + this.kingPos[oppCol] = [-1,-1]; + this.castleFlags[oppCol] = [false,false]; + } + // Did we magnetically move our (init) rooks or opponents' ones ? + const firstRank = (c == "w" ? 7 : 0); + const oppFirstRank = 7 - firstRank; + const oppCol = V.GetOppCol(c); + move.vanish.forEach(psq => { + if (psq.x == firstRank && this.INIT_COL_ROOK[c].includes(psq.y)) + this.castleFlags[c][psq.y==this.INIT_COL_ROOK[c][0] ? 0 : 1] = false; + else if (psq.x == oppFirstRank && this.INIT_COL_ROOK[oppCol].includes(psq.y)) + this.castleFlags[oppCol][psq.y==this.INIT_COL_ROOK[oppCol][0] ? 0 : 1] = false; + }); + } - unupdateVariables(move) - { - super.unupdateVariables(move); - const c = move.vanish[0].c; - const oppCol = V.GetOppCol(c); - if (this.kingPos[oppCol][0] < 0) - { - // Last move took opponent's king - for (let psq of move.vanish) - { - if (psq.p == 'k') - { - this.kingPos[oppCol] = [psq.x, psq.y]; - break; - } - } - } - } + unupdateVariables(move) + { + super.unupdateVariables(move); + const c = move.vanish[0].c; + const oppCol = V.GetOppCol(c); + if (this.kingPos[oppCol][0] < 0) + { + // Last move took opponent's king + for (let psq of move.vanish) + { + if (psq.p == 'k') + { + this.kingPos[oppCol] = [psq.x, psq.y]; + break; + } + } + } + } - getCurrentScore() - { - const color = this.turn; - const kp = this.kingPos[color]; - if (kp[0] < 0) //king disappeared - return (color == "w" ? "0-1" : "1-0"); + getCurrentScore() + { + const color = this.turn; + const kp = this.kingPos[color]; + if (kp[0] < 0) //king disappeared + return (color == "w" ? "0-1" : "1-0"); if (this.atLeastOneMove()) // game not over return "*"; return "1/2"; //no moves but kings still there - } + } - static get THRESHOLD_MATE() - { - return 500; //checkmates evals may be slightly below 1000 - } + static get THRESHOLD_MATE() + { + return 500; //checkmates evals may be slightly below 1000 + } } diff --git a/client/src/variants/Marseille.js b/client/src/variants/Marseille.js index ac1782f1..31c363d2 100644 --- a/client/src/variants/Marseille.js +++ b/client/src/variants/Marseille.js @@ -3,297 +3,297 @@ import { randInt } from "@/utils/alea"; export const VariantRules = class MarseilleRules extends ChessRules { - static IsGoodEnpassant(enpassant) - { - if (enpassant != "-") - { - const squares = enpassant.split(","); - if (squares.length > 2) - return false; - for (let sq of squares) - { - const ep = V.SquareToCoords(sq); - if (isNaN(ep.x) || !V.OnBoard(ep)) - return false; - } - } - return true; - } + static IsGoodEnpassant(enpassant) + { + if (enpassant != "-") + { + const squares = enpassant.split(","); + if (squares.length > 2) + return false; + for (let sq of squares) + { + const ep = V.SquareToCoords(sq); + if (isNaN(ep.x) || !V.OnBoard(ep)) + return false; + } + } + return true; + } - getTurnFen() - { - return this.turn + this.subTurn; - } + getTurnFen() + { + return this.turn + this.subTurn; + } - // There may be 2 enPassant squares (if 2 pawns jump 2 squares in same turn) - getEnpassantFen() - { - const L = this.epSquares.length; - if (this.epSquares[L-1].every(epsq => epsq === undefined)) - return "-"; //no en-passant - let res = ""; - this.epSquares[L-1].forEach(epsq => { - if (!!epsq) - res += V.CoordsToSquare(epsq) + ","; - }); - return res.slice(0,-1); //remove last comma - } + // There may be 2 enPassant squares (if 2 pawns jump 2 squares in same turn) + getEnpassantFen() + { + const L = this.epSquares.length; + if (this.epSquares[L-1].every(epsq => epsq === undefined)) + return "-"; //no en-passant + let res = ""; + this.epSquares[L-1].forEach(epsq => { + if (!!epsq) + res += V.CoordsToSquare(epsq) + ","; + }); + return res.slice(0,-1); //remove last comma + } - setOtherVariables(fen) - { - const parsedFen = V.ParseFen(fen); - this.setFlags(parsedFen.flags); - if (parsedFen.enpassant == "-") - this.epSquares = [ [undefined] ]; - else - { - let res = []; - const squares = parsedFen.enpassant.split(","); - for (let sq of squares) - res.push(V.SquareToCoords(sq)); - this.epSquares = [ res ]; - } - this.scanKingsRooks(fen); - // Extract subTurn from turn indicator: "w" (first move), or - // "w1" or "w2" white subturn 1 or 2, and same for black - const fullTurn = V.ParseFen(fen).turn; - this.turn = fullTurn[0]; - this.subTurn = (fullTurn[1] || 0); //"w0" = special code for first move in game - } + setOtherVariables(fen) + { + const parsedFen = V.ParseFen(fen); + this.setFlags(parsedFen.flags); + if (parsedFen.enpassant == "-") + this.epSquares = [ [undefined] ]; + else + { + let res = []; + const squares = parsedFen.enpassant.split(","); + for (let sq of squares) + res.push(V.SquareToCoords(sq)); + this.epSquares = [ res ]; + } + this.scanKingsRooks(fen); + // Extract subTurn from turn indicator: "w" (first move), or + // "w1" or "w2" white subturn 1 or 2, and same for black + const fullTurn = V.ParseFen(fen).turn; + this.turn = fullTurn[0]; + this.subTurn = (fullTurn[1] || 0); //"w0" = special code for first move in game + } - getPotentialPawnMoves([x,y]) - { - const color = this.turn; - let moves = []; - const [sizeX,sizeY] = [V.size.x,V.size.y]; - const shiftX = (color == "w" ? -1 : 1); - const firstRank = (color == 'w' ? sizeX-1 : 0); - const startRank = (color == "w" ? sizeX-2 : 1); - const lastRank = (color == "w" ? 0 : sizeX-1); - const finalPieces = x + shiftX == lastRank - ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN] - : [V.PAWN]; + getPotentialPawnMoves([x,y]) + { + const color = this.turn; + let moves = []; + const [sizeX,sizeY] = [V.size.x,V.size.y]; + const shiftX = (color == "w" ? -1 : 1); + const firstRank = (color == 'w' ? sizeX-1 : 0); + const startRank = (color == "w" ? sizeX-2 : 1); + const lastRank = (color == "w" ? 0 : sizeX-1); + const finalPieces = x + shiftX == lastRank + ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN] + : [V.PAWN]; - // One square forward - if (this.board[x+shiftX][y] == V.EMPTY) - { - for (let piece of finalPieces) - { - moves.push(this.getBasicMove([x,y], [x+shiftX,y], - {c:color,p:piece})); - } - // Next condition because pawns on 1st rank can generally jump - if ([startRank,firstRank].includes(x) - && this.board[x+2*shiftX][y] == V.EMPTY) - { - // Two squares jump - moves.push(this.getBasicMove([x,y], [x+2*shiftX,y])); - } - } - // Captures - for (let shiftY of [-1,1]) - { - if (y + shiftY >= 0 && y + shiftY < sizeY - && this.board[x+shiftX][y+shiftY] != V.EMPTY - && this.canTake([x,y], [x+shiftX,y+shiftY])) - { - for (let piece of finalPieces) - { - moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], - {c:color,p:piece})); - } - } - } + // One square forward + if (this.board[x+shiftX][y] == V.EMPTY) + { + for (let piece of finalPieces) + { + moves.push(this.getBasicMove([x,y], [x+shiftX,y], + {c:color,p:piece})); + } + // Next condition because pawns on 1st rank can generally jump + if ([startRank,firstRank].includes(x) + && this.board[x+2*shiftX][y] == V.EMPTY) + { + // Two squares jump + moves.push(this.getBasicMove([x,y], [x+2*shiftX,y])); + } + } + // Captures + for (let shiftY of [-1,1]) + { + if (y + shiftY >= 0 && y + shiftY < sizeY + && this.board[x+shiftX][y+shiftY] != V.EMPTY + && this.canTake([x,y], [x+shiftX,y+shiftY])) + { + for (let piece of finalPieces) + { + moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], + {c:color,p:piece})); + } + } + } - // En passant: always OK if subturn 1, - // OK on subturn 2 only if enPassant was played at subturn 1 - // (and if there are two e.p. squares available). - const Lep = this.epSquares.length; - const epSquares = this.epSquares[Lep-1]; //always at least one element - let epSqs = []; - epSquares.forEach(sq => { - if (!!sq) - epSqs.push(sq); - }); - if (epSqs.length == 0) - return moves; - const oppCol = V.GetOppCol(color); - for (let sq of epSqs) - { - if (this.subTurn == 1 || (epSqs.length == 2 && - // Was this en-passant capture already played at subturn 1 ? - // (Or maybe the opponent filled the en-passant square with a piece) - this.board[epSqs[0].x][epSqs[0].y] != V.EMPTY)) - { - if (sq.x == x+shiftX && Math.abs(sq.y - y) == 1 - // Add condition "enemy pawn must be present" - && this.getPiece(x,sq.y) == V.PAWN && this.getColor(x,sq.y) == oppCol) - { - let epMove = this.getBasicMove([x,y], [sq.x,sq.y]); - epMove.vanish.push({ - x: x, - y: sq.y, - p: 'p', - c: oppCol - }); - moves.push(epMove); - } - } - } + // En passant: always OK if subturn 1, + // OK on subturn 2 only if enPassant was played at subturn 1 + // (and if there are two e.p. squares available). + const Lep = this.epSquares.length; + const epSquares = this.epSquares[Lep-1]; //always at least one element + let epSqs = []; + epSquares.forEach(sq => { + if (!!sq) + epSqs.push(sq); + }); + if (epSqs.length == 0) + return moves; + const oppCol = V.GetOppCol(color); + for (let sq of epSqs) + { + if (this.subTurn == 1 || (epSqs.length == 2 && + // Was this en-passant capture already played at subturn 1 ? + // (Or maybe the opponent filled the en-passant square with a piece) + this.board[epSqs[0].x][epSqs[0].y] != V.EMPTY)) + { + if (sq.x == x+shiftX && Math.abs(sq.y - y) == 1 + // Add condition "enemy pawn must be present" + && this.getPiece(x,sq.y) == V.PAWN && this.getColor(x,sq.y) == oppCol) + { + let epMove = this.getBasicMove([x,y], [sq.x,sq.y]); + epMove.vanish.push({ + x: x, + y: sq.y, + p: 'p', + c: oppCol + }); + moves.push(epMove); + } + } + } - return moves; - } + return moves; + } - play(move) - { - move.flags = JSON.stringify(this.aggregateFlags()); - move.turn = this.turn + this.subTurn; - V.PlayOnBoard(this.board, move); - const epSq = this.getEpSquare(move); - if (this.subTurn == 0) //first move in game - { - this.turn = "b"; + play(move) + { + move.flags = JSON.stringify(this.aggregateFlags()); + move.turn = this.turn + this.subTurn; + V.PlayOnBoard(this.board, move); + const epSq = this.getEpSquare(move); + if (this.subTurn == 0) //first move in game + { + this.turn = "b"; this.subTurn = 1; - this.epSquares.push([epSq]); - } - // Does this move give check on subturn 1? If yes, skip subturn 2 - else if (this.subTurn==1 && this.underCheck(V.GetOppCol(this.turn))) - { - this.turn = V.GetOppCol(this.turn); - this.epSquares.push([epSq]); - move.checkOnSubturn1 = true; - } - else - { - if (this.subTurn == 2) - { - this.turn = V.GetOppCol(this.turn); - let lastEpsq = this.epSquares[this.epSquares.length-1]; - lastEpsq.push(epSq); - } - else - this.epSquares.push([epSq]); - this.subTurn = 3 - this.subTurn; - } - this.updateVariables(move); - } + this.epSquares.push([epSq]); + } + // Does this move give check on subturn 1? If yes, skip subturn 2 + else if (this.subTurn==1 && this.underCheck(V.GetOppCol(this.turn))) + { + this.turn = V.GetOppCol(this.turn); + this.epSquares.push([epSq]); + move.checkOnSubturn1 = true; + } + else + { + if (this.subTurn == 2) + { + this.turn = V.GetOppCol(this.turn); + let lastEpsq = this.epSquares[this.epSquares.length-1]; + lastEpsq.push(epSq); + } + else + this.epSquares.push([epSq]); + this.subTurn = 3 - this.subTurn; + } + this.updateVariables(move); + } - undo(move) - { - this.disaggregateFlags(JSON.parse(move.flags)); - V.UndoOnBoard(this.board, move); - if (move.turn[1] == '0' || move.checkOnSubturn1 || this.subTurn == 2) - this.epSquares.pop(); - else //this.subTurn == 1 - { - let lastEpsq = this.epSquares[this.epSquares.length-1]; - lastEpsq.pop(); - } - this.turn = move.turn[0]; - this.subTurn = parseInt(move.turn[1]); - this.unupdateVariables(move); - } + undo(move) + { + this.disaggregateFlags(JSON.parse(move.flags)); + V.UndoOnBoard(this.board, move); + if (move.turn[1] == '0' || move.checkOnSubturn1 || this.subTurn == 2) + this.epSquares.pop(); + else //this.subTurn == 1 + { + let lastEpsq = this.epSquares[this.epSquares.length-1]; + lastEpsq.pop(); + } + this.turn = move.turn[0]; + this.subTurn = parseInt(move.turn[1]); + this.unupdateVariables(move); + } - // NOTE: GenRandInitFen() is OK, - // since at first move turn indicator is just "w" + // NOTE: GenRandInitFen() is OK, + // since at first move turn indicator is just "w" - static get VALUES() - { - return { - 'p': 1, - 'r': 5, - 'n': 3, - 'b': 3, - 'q': 7, //slightly less than in orthodox game - 'k': 1000 - }; - } + static get VALUES() + { + return { + 'p': 1, + 'r': 5, + 'n': 3, + 'b': 3, + 'q': 7, //slightly less than in orthodox game + 'k': 1000 + }; + } - // No alpha-beta here, just adapted min-max at depth 2(+1) - getComputerMove() - { - if (this.subTurn == 2) - return null; //TODO: imperfect interface setup + // No alpha-beta here, just adapted min-max at depth 2(+1) + getComputerMove() + { + if (this.subTurn == 2) + return null; //TODO: imperfect interface setup - const maxeval = V.INFINITY; - const color = this.turn; - const oppCol = V.GetOppCol(this.turn); + const maxeval = V.INFINITY; + const color = this.turn; + const oppCol = V.GetOppCol(this.turn); - // Search best (half) move for opponent turn - const getBestMoveEval = () => { - const turnBefore = this.turn + this.subTurn; - let score = this.getCurrentScore(); - if (score != "*") - { - if (score == "1/2") - return 0; - return maxeval * (score == "1-0" ? 1 : -1); - } - let moves = this.getAllValidMoves(); - let res = (oppCol == "w" ? -maxeval : maxeval); - for (let m of moves) - { - this.play(m); - score = this.getCurrentScore(); - // Now turn is oppCol,2 if m doesn't give check - // Otherwise it's color,1. In both cases the next test makes sense - if (score != "*") - { - if (score == "1/2") - res = (oppCol == "w" ? Math.max(res, 0) : Math.min(res, 0)); - else - { - // Found a mate - this.undo(m); - return maxeval * (score == "1-0" ? 1 : -1); - } - } - const evalPos = this.evalPosition(); - res = (oppCol == "w" ? Math.max(res, evalPos) : Math.min(res, evalPos)); - this.undo(m); - } - return res; - }; + // Search best (half) move for opponent turn + const getBestMoveEval = () => { + const turnBefore = this.turn + this.subTurn; + let score = this.getCurrentScore(); + if (score != "*") + { + if (score == "1/2") + return 0; + return maxeval * (score == "1-0" ? 1 : -1); + } + let moves = this.getAllValidMoves(); + let res = (oppCol == "w" ? -maxeval : maxeval); + for (let m of moves) + { + this.play(m); + score = this.getCurrentScore(); + // Now turn is oppCol,2 if m doesn't give check + // Otherwise it's color,1. In both cases the next test makes sense + if (score != "*") + { + if (score == "1/2") + res = (oppCol == "w" ? Math.max(res, 0) : Math.min(res, 0)); + else + { + // Found a mate + this.undo(m); + return maxeval * (score == "1-0" ? 1 : -1); + } + } + const evalPos = this.evalPosition(); + res = (oppCol == "w" ? Math.max(res, evalPos) : Math.min(res, evalPos)); + this.undo(m); + } + return res; + }; - let moves11 = this.getAllValidMoves(); - let doubleMoves = []; - // Rank moves using a min-max at depth 2 - for (let i=0; i<moves11.length; i++) - { - this.play(moves11[i]); - if (this.turn != color) - { - // We gave check with last move: search the best opponent move - doubleMoves.push({moves:[moves11[i]], eval:getBestMoveEval()}); - } - else - { - let moves12 = this.getAllValidMoves(); - for (let j=0; j<moves12.length; j++) - { - this.play(moves12[j]); - doubleMoves.push({ - moves:[moves11[i],moves12[j]], - eval:getBestMoveEval()}); - this.undo(moves12[j]); - } - } - this.undo(moves11[i]); - } + let moves11 = this.getAllValidMoves(); + let doubleMoves = []; + // Rank moves using a min-max at depth 2 + for (let i=0; i<moves11.length; i++) + { + this.play(moves11[i]); + if (this.turn != color) + { + // We gave check with last move: search the best opponent move + doubleMoves.push({moves:[moves11[i]], eval:getBestMoveEval()}); + } + else + { + let moves12 = this.getAllValidMoves(); + for (let j=0; j<moves12.length; j++) + { + this.play(moves12[j]); + doubleMoves.push({ + moves:[moves11[i],moves12[j]], + eval:getBestMoveEval()}); + this.undo(moves12[j]); + } + } + this.undo(moves11[i]); + } - doubleMoves.sort( (a,b) => { - return (color=="w" ? 1 : -1) * (b.eval - a.eval); }); - let candidates = [0]; //indices of candidates moves - for (let i=1; - i<doubleMoves.length && doubleMoves[i].eval == doubleMoves[0].eval; - i++) - { - candidates.push(i); - } + doubleMoves.sort( (a,b) => { + return (color=="w" ? 1 : -1) * (b.eval - a.eval); }); + let candidates = [0]; //indices of candidates moves + for (let i=1; + i<doubleMoves.length && doubleMoves[i].eval == doubleMoves[0].eval; + i++) + { + candidates.push(i); + } - const selected = doubleMoves[randInt(candidates.length)].moves; - if (selected.length == 1) - return selected[0]; - return selected; - } + const selected = doubleMoves[randInt(candidates.length)].moves; + if (selected.length == 1) + return selected[0]; + return selected; + } } diff --git a/client/src/variants/Upsidedown.js b/client/src/variants/Upsidedown.js index de722168..bf3e37c4 100644 --- a/client/src/variants/Upsidedown.js +++ b/client/src/variants/Upsidedown.js @@ -4,71 +4,71 @@ import { ArrayFun } from "@/utils/array"; export const VariantRules = class UpsidedownRules extends ChessRules { - static get HasFlags() { return false; } + static get HasFlags() { return false; } - static get HasEnpassant() { return false; } + static get HasEnpassant() { return false; } - getPotentialKingMoves(sq) - { - // No castle - return this.getSlideNJumpMoves(sq, - V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); - } + getPotentialKingMoves(sq) + { + // No castle + return this.getSlideNJumpMoves(sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + } - static GenRandInitFen() - { - let pieces = { "w": new Array(8), "b": new Array(8) }; - for (let c of ["w","b"]) - { - let positions = ArrayFun.range(8); + static GenRandInitFen() + { + let pieces = { "w": new Array(8), "b": new Array(8) }; + for (let c of ["w","b"]) + { + let positions = ArrayFun.range(8); - let randIndex = randInt(8); - const kingPos = positions[randIndex]; - positions.splice(randIndex, 1); + let randIndex = randInt(8); + const kingPos = positions[randIndex]; + positions.splice(randIndex, 1); - // At least a knight must be next to the king: - let knight1Pos = undefined; - if (kingPos == 0) - knight1Pos = 1; - else if (kingPos == V.size.y-1) - knight1Pos = V.size.y-2; - else - knight1Pos = kingPos + (Math.random() < 0.5 ? 1 : -1); - // Search for knight1Pos index in positions and remove it - const knight1Index = positions.indexOf(knight1Pos); - positions.splice(knight1Index, 1); + // At least a knight must be next to the king: + let knight1Pos = undefined; + if (kingPos == 0) + knight1Pos = 1; + else if (kingPos == V.size.y-1) + knight1Pos = V.size.y-2; + else + knight1Pos = kingPos + (Math.random() < 0.5 ? 1 : -1); + // Search for knight1Pos index in positions and remove it + const knight1Index = positions.indexOf(knight1Pos); + positions.splice(knight1Index, 1); - // King+knight1 are on two consecutive squares: one light, one dark - randIndex = 2 * randInt(3); - const bishop1Pos = positions[randIndex]; - let randIndex_tmp = 2 * randInt(3) + 1; - const bishop2Pos = positions[randIndex_tmp]; - positions.splice(Math.max(randIndex,randIndex_tmp), 1); - positions.splice(Math.min(randIndex,randIndex_tmp), 1); + // King+knight1 are on two consecutive squares: one light, one dark + randIndex = 2 * randInt(3); + const bishop1Pos = positions[randIndex]; + let randIndex_tmp = 2 * randInt(3) + 1; + const bishop2Pos = positions[randIndex_tmp]; + positions.splice(Math.max(randIndex,randIndex_tmp), 1); + positions.splice(Math.min(randIndex,randIndex_tmp), 1); - randIndex = randInt(4); - const knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); + randIndex = randInt(4); + const knight2Pos = positions[randIndex]; + positions.splice(randIndex, 1); - randIndex = randInt(3); - const queenPos = positions[randIndex]; - positions.splice(randIndex, 1); + randIndex = randInt(3); + const queenPos = positions[randIndex]; + positions.splice(randIndex, 1); - const rook1Pos = positions[0]; - const rook2Pos = positions[1]; + const rook1Pos = positions[0]; + const rook2Pos = positions[1]; - pieces[c][rook1Pos] = 'r'; - pieces[c][knight1Pos] = 'n'; - pieces[c][bishop1Pos] = 'b'; - pieces[c][queenPos] = 'q'; - pieces[c][kingPos] = 'k'; - pieces[c][bishop2Pos] = 'b'; - pieces[c][knight2Pos] = 'n'; - pieces[c][rook2Pos] = 'r'; - } - return pieces["w"].join("").toUpperCase() + - "/PPPPPPPP/8/8/8/8/pppppppp/" + - pieces["b"].join("") + - " w 0"; //no castle, no en-passant - } + pieces[c][rook1Pos] = 'r'; + pieces[c][knight1Pos] = 'n'; + pieces[c][bishop1Pos] = 'b'; + pieces[c][queenPos] = 'q'; + pieces[c][kingPos] = 'k'; + pieces[c][bishop2Pos] = 'b'; + pieces[c][knight2Pos] = 'n'; + pieces[c][rook2Pos] = 'r'; + } + return pieces["w"].join("").toUpperCase() + + "/PPPPPPPP/8/8/8/8/pppppppp/" + + pieces["b"].join("") + + " w 0"; //no castle, no en-passant + } } diff --git a/client/src/variants/Wildebeest.js b/client/src/variants/Wildebeest.js index 20a32ccf..a30e7f91 100644 --- a/client/src/variants/Wildebeest.js +++ b/client/src/variants/Wildebeest.js @@ -4,293 +4,293 @@ import { sample, randInt } from "@/utils/alea"; export const VariantRules = class WildebeestRules extends ChessRules { - static getPpath(b) - { - return ([V.CAMEL,V.WILDEBEEST].includes(b[1]) ? "Wildebeest/" : "") + b; - } + static getPpath(b) + { + return ([V.CAMEL,V.WILDEBEEST].includes(b[1]) ? "Wildebeest/" : "") + b; + } - static get size() { return {x:10,y:11}; } + static get size() { return {x:10,y:11}; } - static get CAMEL() { return 'c'; } - static get WILDEBEEST() { return 'w'; } + static get CAMEL() { return 'c'; } + static get WILDEBEEST() { return 'w'; } - static get PIECES() - { - return ChessRules.PIECES.concat([V.CAMEL,V.WILDEBEEST]); - } + static get PIECES() + { + return ChessRules.PIECES.concat([V.CAMEL,V.WILDEBEEST]); + } - static get steps() - { - return Object.assign( - ChessRules.steps, //add camel moves: - {'c': [ [-3,-1],[-3,1],[-1,-3],[-1,3],[1,-3],[1,3],[3,-1],[3,1] ]} - ); - } + static get steps() + { + return Object.assign( + ChessRules.steps, //add camel moves: + {'c': [ [-3,-1],[-3,1],[-1,-3],[-1,3],[1,-3],[1,3],[3,-1],[3,1] ]} + ); + } - static IsGoodEnpassant(enpassant) - { - if (enpassant != "-") - { - const squares = enpassant.split(","); - if (squares.length > 2) - return false; - for (let sq of squares) - { - const ep = V.SquareToCoords(sq); - if (isNaN(ep.x) || !V.OnBoard(ep)) - return false; - } - } - return true; - } + static IsGoodEnpassant(enpassant) + { + if (enpassant != "-") + { + const squares = enpassant.split(","); + if (squares.length > 2) + return false; + for (let sq of squares) + { + const ep = V.SquareToCoords(sq); + if (isNaN(ep.x) || !V.OnBoard(ep)) + return false; + } + } + return true; + } - // There may be 2 enPassant squares (if pawn jump 3 squares) - getEnpassantFen() - { - const L = this.epSquares.length; - if (!this.epSquares[L-1]) - return "-"; //no en-passant - let res = ""; - this.epSquares[L-1].forEach(sq => { - res += V.CoordsToSquare(sq) + ","; - }); - return res.slice(0,-1); //remove last comma - } + // There may be 2 enPassant squares (if pawn jump 3 squares) + getEnpassantFen() + { + const L = this.epSquares.length; + if (!this.epSquares[L-1]) + return "-"; //no en-passant + let res = ""; + this.epSquares[L-1].forEach(sq => { + res += V.CoordsToSquare(sq) + ","; + }); + return res.slice(0,-1); //remove last comma + } - // En-passant after 2-sq or 3-sq jumps - getEpSquare(moveOrSquare) - { - if (!moveOrSquare) - return undefined; - if (typeof moveOrSquare === "string") - { - const square = moveOrSquare; - if (square == "-") - return undefined; - let res = []; - square.split(",").forEach(sq => { - res.push(V.SquareToCoords(sq)); - }); - return res; - } - // Argument is a move: - const move = moveOrSquare; - const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; - if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) >= 2) - { - const step = (ex-sx) / Math.abs(ex-sx); - let res = [{ - x: sx + step, - y: sy - }]; - if (sx + 2*step != ex) //3-squares move - { - res.push({ - x: sx + 2*step, - y: sy - }); - } - return res; - } - return undefined; //default - } + // En-passant after 2-sq or 3-sq jumps + getEpSquare(moveOrSquare) + { + if (!moveOrSquare) + return undefined; + if (typeof moveOrSquare === "string") + { + const square = moveOrSquare; + if (square == "-") + return undefined; + let res = []; + square.split(",").forEach(sq => { + res.push(V.SquareToCoords(sq)); + }); + return res; + } + // Argument is a move: + const move = moveOrSquare; + const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; + if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) >= 2) + { + const step = (ex-sx) / Math.abs(ex-sx); + let res = [{ + x: sx + step, + y: sy + }]; + if (sx + 2*step != ex) //3-squares move + { + res.push({ + x: sx + 2*step, + y: sy + }); + } + return res; + } + return undefined; //default + } - getPotentialMovesFrom([x,y]) - { - switch (this.getPiece(x,y)) - { - case V.CAMEL: - return this.getPotentialCamelMoves([x,y]); - case V.WILDEBEEST: - return this.getPotentialWildebeestMoves([x,y]); - default: - return super.getPotentialMovesFrom([x,y]) - } - } + getPotentialMovesFrom([x,y]) + { + switch (this.getPiece(x,y)) + { + case V.CAMEL: + return this.getPotentialCamelMoves([x,y]); + case V.WILDEBEEST: + return this.getPotentialWildebeestMoves([x,y]); + default: + return super.getPotentialMovesFrom([x,y]) + } + } - // Pawns jump 2 or 3 squares, and promote to queen or wildebeest - getPotentialPawnMoves([x,y]) - { - const color = this.turn; - let moves = []; - const [sizeX,sizeY] = [V.size.x,V.size.y]; - const shiftX = (color == "w" ? -1 : 1); - const startRanks = (color == "w" ? [sizeX-2,sizeX-3] : [1,2]); - const lastRank = (color == "w" ? 0 : sizeX-1); - const finalPieces = x + shiftX == lastRank - ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN] - : [V.PAWN]; + // Pawns jump 2 or 3 squares, and promote to queen or wildebeest + getPotentialPawnMoves([x,y]) + { + const color = this.turn; + let moves = []; + const [sizeX,sizeY] = [V.size.x,V.size.y]; + const shiftX = (color == "w" ? -1 : 1); + const startRanks = (color == "w" ? [sizeX-2,sizeX-3] : [1,2]); + const lastRank = (color == "w" ? 0 : sizeX-1); + const finalPieces = x + shiftX == lastRank + ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN] + : [V.PAWN]; - if (this.board[x+shiftX][y] == V.EMPTY) - { - // One square forward - for (let piece of finalPieces) - moves.push(this.getBasicMove([x,y], [x+shiftX,y], {c:color,p:piece})); - if (startRanks.includes(x)) - { - if (this.board[x+2*shiftX][y] == V.EMPTY) - { - // Two squares jump - moves.push(this.getBasicMove([x,y], [x+2*shiftX,y])); - if (x==startRanks[0] && this.board[x+3*shiftX][y] == V.EMPTY) - { - // Three squares jump - moves.push(this.getBasicMove([x,y], [x+3*shiftX,y])); - } - } - } - } - // Captures - for (let shiftY of [-1,1]) - { - if (y + shiftY >= 0 && y + shiftY < sizeY - && this.board[x+shiftX][y+shiftY] != V.EMPTY - && this.canTake([x,y], [x+shiftX,y+shiftY])) - { - for (let piece of finalPieces) - { - moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], - {c:color,p:piece})); - } - } - } + if (this.board[x+shiftX][y] == V.EMPTY) + { + // One square forward + for (let piece of finalPieces) + moves.push(this.getBasicMove([x,y], [x+shiftX,y], {c:color,p:piece})); + if (startRanks.includes(x)) + { + if (this.board[x+2*shiftX][y] == V.EMPTY) + { + // Two squares jump + moves.push(this.getBasicMove([x,y], [x+2*shiftX,y])); + if (x==startRanks[0] && this.board[x+3*shiftX][y] == V.EMPTY) + { + // Three squares jump + moves.push(this.getBasicMove([x,y], [x+3*shiftX,y])); + } + } + } + } + // Captures + for (let shiftY of [-1,1]) + { + if (y + shiftY >= 0 && y + shiftY < sizeY + && this.board[x+shiftX][y+shiftY] != V.EMPTY + && this.canTake([x,y], [x+shiftX,y+shiftY])) + { + for (let piece of finalPieces) + { + moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY], + {c:color,p:piece})); + } + } + } - // En passant - const Lep = this.epSquares.length; - const epSquare = this.epSquares[Lep-1]; - if (!!epSquare) - { - for (let epsq of epSquare) - { - // TODO: some redundant checks - if (epsq.x == x+shiftX && Math.abs(epsq.y - y) == 1) - { - var enpassantMove = this.getBasicMove([x,y], [epsq.x,epsq.y]); - // WARNING: the captured pawn may be diagonally behind us, - // if it's a 3-squares jump and we take on 1st passing square - const px = (this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX); - enpassantMove.vanish.push({ - x: px, - y: epsq.y, - p: 'p', - c: this.getColor(px,epsq.y) - }); - moves.push(enpassantMove); - } - } - } + // En passant + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep-1]; + if (!!epSquare) + { + for (let epsq of epSquare) + { + // TODO: some redundant checks + if (epsq.x == x+shiftX && Math.abs(epsq.y - y) == 1) + { + var enpassantMove = this.getBasicMove([x,y], [epsq.x,epsq.y]); + // WARNING: the captured pawn may be diagonally behind us, + // if it's a 3-squares jump and we take on 1st passing square + const px = (this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX); + enpassantMove.vanish.push({ + x: px, + y: epsq.y, + p: 'p', + c: this.getColor(px,epsq.y) + }); + moves.push(enpassantMove); + } + } + } - return moves; - } + return moves; + } - // TODO: wildebeest castle + // TODO: wildebeest castle - getPotentialCamelMoves(sq) - { - return this.getSlideNJumpMoves(sq, V.steps[V.CAMEL], "oneStep"); - } + getPotentialCamelMoves(sq) + { + return this.getSlideNJumpMoves(sq, V.steps[V.CAMEL], "oneStep"); + } - getPotentialWildebeestMoves(sq) - { - return this.getSlideNJumpMoves( - sq, V.steps[V.KNIGHT].concat(V.steps[V.CAMEL]), "oneStep"); - } + getPotentialWildebeestMoves(sq) + { + return this.getSlideNJumpMoves( + sq, V.steps[V.KNIGHT].concat(V.steps[V.CAMEL]), "oneStep"); + } - isAttacked(sq, colors) - { - return super.isAttacked(sq, colors) - || this.isAttackedByCamel(sq, colors) - || this.isAttackedByWildebeest(sq, colors); - } + isAttacked(sq, colors) + { + return super.isAttacked(sq, colors) + || this.isAttackedByCamel(sq, colors) + || this.isAttackedByWildebeest(sq, colors); + } - isAttackedByCamel(sq, colors) - { - return this.isAttackedBySlideNJump(sq, colors, - V.CAMEL, V.steps[V.CAMEL], "oneStep"); - } + isAttackedByCamel(sq, colors) + { + return this.isAttackedBySlideNJump(sq, colors, + V.CAMEL, V.steps[V.CAMEL], "oneStep"); + } - isAttackedByWildebeest(sq, colors) - { - return this.isAttackedBySlideNJump(sq, colors, V.WILDEBEEST, - V.steps[V.KNIGHT].concat(V.steps[V.CAMEL]), "oneStep"); - } + isAttackedByWildebeest(sq, colors) + { + return this.isAttackedBySlideNJump(sq, colors, V.WILDEBEEST, + V.steps[V.KNIGHT].concat(V.steps[V.CAMEL]), "oneStep"); + } getCurrentScore() { if (this.atLeastOneMove()) // game not over return "*"; - // No valid move: game is lost (stalemate is a win) - return (this.turn == "w" ? "0-1" : "1-0"); - } + // No valid move: game is lost (stalemate is a win) + return (this.turn == "w" ? "0-1" : "1-0"); + } - static get VALUES() { - return Object.assign( - ChessRules.VALUES, - {'c': 3, 'w': 7} //experimental - ); - } + static get VALUES() { + return Object.assign( + ChessRules.VALUES, + {'c': 3, 'w': 7} //experimental + ); + } - static get SEARCH_DEPTH() { return 2; } + static get SEARCH_DEPTH() { return 2; } - static GenRandInitFen() - { - let pieces = { "w": new Array(10), "b": new Array(10) }; - for (let c of ["w","b"]) - { - let positions = ArrayFun.range(11); + static GenRandInitFen() + { + let pieces = { "w": new Array(10), "b": new Array(10) }; + for (let c of ["w","b"]) + { + let positions = ArrayFun.range(11); - // Get random squares for bishops + camels (different colors) - let randIndexes = sample(ArrayFun.range(6), 2) + // Get random squares for bishops + camels (different colors) + let randIndexes = sample(ArrayFun.range(6), 2) .map(i => { return 2*i; }); - let bishop1Pos = positions[randIndexes[0]]; - let camel1Pos = positions[randIndexes[1]]; - // The second bishop (camel) must be on a square of different color - let randIndexes_tmp = sample(ArrayFun.range(5), 2) + let bishop1Pos = positions[randIndexes[0]]; + let camel1Pos = positions[randIndexes[1]]; + // The second bishop (camel) must be on a square of different color + let randIndexes_tmp = sample(ArrayFun.range(5), 2) .map(i => { return 2*i+1; }); - let bishop2Pos = positions[randIndexes_tmp[0]]; - let camel2Pos = positions[randIndexes_tmp[1]]; - for (let idx of randIndexes.concat(randIndexes_tmp) - .sort((a,b) => { return b-a; })) //largest indices first - { - positions.splice(idx, 1); - } + let bishop2Pos = positions[randIndexes_tmp[0]]; + let camel2Pos = positions[randIndexes_tmp[1]]; + for (let idx of randIndexes.concat(randIndexes_tmp) + .sort((a,b) => { return b-a; })) //largest indices first + { + positions.splice(idx, 1); + } - let randIndex = randInt(7); - let knight1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - randIndex = randInt(6); - let knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); + let randIndex = randInt(7); + let knight1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + randIndex = randInt(6); + let knight2Pos = positions[randIndex]; + positions.splice(randIndex, 1); - randIndex = randInt(5); - let queenPos = positions[randIndex]; - positions.splice(randIndex, 1); + randIndex = randInt(5); + let queenPos = positions[randIndex]; + positions.splice(randIndex, 1); - // Random square for wildebeest - randIndex = randInt(4); - let wildebeestPos = positions[randIndex]; - positions.splice(randIndex, 1); + // Random square for wildebeest + randIndex = randInt(4); + let wildebeestPos = positions[randIndex]; + positions.splice(randIndex, 1); - let rook1Pos = positions[0]; - let kingPos = positions[1]; - let rook2Pos = positions[2]; + let rook1Pos = positions[0]; + let kingPos = positions[1]; + let rook2Pos = positions[2]; - pieces[c][rook1Pos] = 'r'; - pieces[c][knight1Pos] = 'n'; - pieces[c][bishop1Pos] = 'b'; - pieces[c][queenPos] = 'q'; - pieces[c][camel1Pos] = 'c'; - pieces[c][camel2Pos] = 'c'; - pieces[c][wildebeestPos] = 'w'; - pieces[c][kingPos] = 'k'; - pieces[c][bishop2Pos] = 'b'; - pieces[c][knight2Pos] = 'n'; - pieces[c][rook2Pos] = 'r'; - } - return pieces["b"].join("") + - "/ppppppppppp/11/11/11/11/11/11/PPPPPPPPPPP/" + - pieces["w"].join("").toUpperCase() + - " w 0 1111 -"; - } + pieces[c][rook1Pos] = 'r'; + pieces[c][knight1Pos] = 'n'; + pieces[c][bishop1Pos] = 'b'; + pieces[c][queenPos] = 'q'; + pieces[c][camel1Pos] = 'c'; + pieces[c][camel2Pos] = 'c'; + pieces[c][wildebeestPos] = 'w'; + pieces[c][kingPos] = 'k'; + pieces[c][bishop2Pos] = 'b'; + pieces[c][knight2Pos] = 'n'; + pieces[c][rook2Pos] = 'r'; + } + return pieces["b"].join("") + + "/ppppppppppp/11/11/11/11/11/11/PPPPPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + " w 0 1111 -"; + } } diff --git a/client/src/variants/Zen.js b/client/src/variants/Zen.js index db2146a7..b8ce0713 100644 --- a/client/src/variants/Zen.js +++ b/client/src/variants/Zen.js @@ -2,228 +2,228 @@ import { ChessRules } from "@/base_rules"; export const VariantRules = class ZenRules extends ChessRules { - // NOTE: enPassant, if enabled, would need to redefine carefully getEpSquare - static get HasEnpassant() { return false; } - - // TODO(?): some duplicated code in 2 next functions - getSlideNJumpMoves([x,y], steps, oneStep) - { - const color = this.getColor(x,y); - let moves = []; - outerLoop: - for (let loop=0; loop<steps.length; loop++) - { - const step = steps[loop]; - let i = x + step[0]; - let j = y + step[1]; - while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) - { - moves.push(this.getBasicMove([x,y], [i,j])); - if (!!oneStep) - continue outerLoop; - i += step[0]; - j += step[1]; - } - // No capture check: handled elsewhere (next method) - } - return moves; - } - - // follow steps from x,y until something is met. - // if met piece is opponent and same movement (asA): eat it! - findCaptures_aux([x,y], asA) - { - const color = this.getColor(x,y); - var moves = []; - const steps = asA != V.PAWN - ? (asA==V.QUEEN ? V.steps[V.ROOK].concat(V.steps[V.BISHOP]) : V.steps[asA]) - : color=='w' ? [[-1,-1],[-1,1]] : [[1,-1],[1,1]]; - const oneStep = (asA==V.KNIGHT || asA==V.PAWN); //we don't capture king - const lastRank = (color == 'w' ? 0 : V.size.x-1); - const promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]; - outerLoop: - for (let loop=0; loop<steps.length; loop++) - { - const step = steps[loop]; - let i = x + step[0]; - let j = y + step[1]; - while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) - { - if (oneStep) - continue outerLoop; - i += step[0]; - j += step[1]; - } - if (V.OnBoard(i,j) && this.getColor(i,j) == V.GetOppCol(color) - && this.getPiece(i,j) == asA) - { - // eat! - if (this.getPiece(x,y) == V.PAWN && i == lastRank) - { - // Special case of promotion: - promotionPieces.forEach(p => { - moves.push(this.getBasicMove([x,y], [i,j], {c:color,p:p})); - }); - } - else - { - // All other cases - moves.push(this.getBasicMove([x,y], [i,j])); - } - } - } - return moves; - } - - // Find possible captures from a square: look in every direction! - findCaptures(sq) - { - let moves = []; - - Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.PAWN)); - Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.ROOK)); - Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.KNIGHT)); - Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.BISHOP)); - Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.QUEEN)); - - return moves; - } - - getPotentialPawnMoves([x,y]) - { - const color = this.getColor(x,y); - let moves = []; - const [sizeX,sizeY] = [V.size.x,V.size.y]; - const shift = (color == 'w' ? -1 : 1); - const startRank = (color == 'w' ? sizeY-2 : 1); - const firstRank = (color == 'w' ? sizeY-1 : 0); - const lastRank = (color == "w" ? 0 : sizeY-1); - - if (x+shift != lastRank) - { - // Normal moves - if (this.board[x+shift][y] == V.EMPTY) - { - moves.push(this.getBasicMove([x,y], [x+shift,y])); - if ([startRank,firstRank].includes(x) && this.board[x+2*shift][y] == V.EMPTY) - { - //two squares jump - moves.push(this.getBasicMove([x,y], [x+2*shift,y])); - } - } - } - - else //promotion - { - let promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]; - promotionPieces.forEach(p => { - // Normal move - if (this.board[x+shift][y] == V.EMPTY) - moves.push(this.getBasicMove([x,y], [x+shift,y], {c:color,p:p})); - }); - } - - // No en passant here - - // Add "zen" captures - Array.prototype.push.apply(moves, this.findCaptures([x,y])); - - return moves; - } - - getPotentialRookMoves(sq) - { - let noCaptures = this.getSlideNJumpMoves(sq, V.steps[V.ROOK]); - let captures = this.findCaptures(sq); - return noCaptures.concat(captures); - } - - getPotentialKnightMoves(sq) - { - let noCaptures = this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep"); - let captures = this.findCaptures(sq); - return noCaptures.concat(captures); - } - - getPotentialBishopMoves(sq) - { - let noCaptures = this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]); - let captures = this.findCaptures(sq); - return noCaptures.concat(captures); - } - - getPotentialQueenMoves(sq) - { - let noCaptures = this.getSlideNJumpMoves( - sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP])); - let captures = this.findCaptures(sq); - return noCaptures.concat(captures); - } - - getPotentialKingMoves(sq) - { - // Initialize with normal moves - let noCaptures = this.getSlideNJumpMoves(sq, - V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); - let captures = this.findCaptures(sq); - return noCaptures.concat(captures).concat(this.getCastleMoves(sq)); - } - - getNotation(move) - { - // Recognize special moves first - if (move.appear.length == 2) - { - // castle - if (move.end.y < move.start.y) - return "0-0-0"; - else - return "0-0"; - } - - // Translate initial square (because pieces may fly unusually in this variant!) - const initialSquare = V.CoordsToSquare(move.start); - - // Translate final square - const finalSquare = V.CoordsToSquare(move.end); - - let notation = ""; - const piece = this.getPiece(move.start.x, move.start.y); - if (piece == V.PAWN) - { - // pawn move (TODO: enPassant indication) - if (move.vanish.length > 1) - { - // capture - notation = initialSquare + "x" + finalSquare; - } - else //no capture - notation = finalSquare; - if (piece != move.appear[0].p) //promotion - notation += "=" + move.appear[0].p.toUpperCase(); - } - - else - { - // Piece movement - notation = piece.toUpperCase(); - if (move.vanish.length > 1) - notation += initialSquare + "x"; - notation += finalSquare; - } - return notation; - } - - static get VALUES() - { - // TODO: experimental - return { - 'p': 1, - 'r': 3, - 'n': 2, - 'b': 2, - 'q': 5, - 'k': 1000 - } - } + // NOTE: enPassant, if enabled, would need to redefine carefully getEpSquare + static get HasEnpassant() { return false; } + + // TODO(?): some duplicated code in 2 next functions + getSlideNJumpMoves([x,y], steps, oneStep) + { + const color = this.getColor(x,y); + let moves = []; + outerLoop: + for (let loop=0; loop<steps.length; loop++) + { + const step = steps[loop]; + let i = x + step[0]; + let j = y + step[1]; + while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) + { + moves.push(this.getBasicMove([x,y], [i,j])); + if (!!oneStep) + continue outerLoop; + i += step[0]; + j += step[1]; + } + // No capture check: handled elsewhere (next method) + } + return moves; + } + + // follow steps from x,y until something is met. + // if met piece is opponent and same movement (asA): eat it! + findCaptures_aux([x,y], asA) + { + const color = this.getColor(x,y); + var moves = []; + const steps = asA != V.PAWN + ? (asA==V.QUEEN ? V.steps[V.ROOK].concat(V.steps[V.BISHOP]) : V.steps[asA]) + : color=='w' ? [[-1,-1],[-1,1]] : [[1,-1],[1,1]]; + const oneStep = (asA==V.KNIGHT || asA==V.PAWN); //we don't capture king + const lastRank = (color == 'w' ? 0 : V.size.x-1); + const promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]; + outerLoop: + for (let loop=0; loop<steps.length; loop++) + { + const step = steps[loop]; + let i = x + step[0]; + let j = y + step[1]; + while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY) + { + if (oneStep) + continue outerLoop; + i += step[0]; + j += step[1]; + } + if (V.OnBoard(i,j) && this.getColor(i,j) == V.GetOppCol(color) + && this.getPiece(i,j) == asA) + { + // eat! + if (this.getPiece(x,y) == V.PAWN && i == lastRank) + { + // Special case of promotion: + promotionPieces.forEach(p => { + moves.push(this.getBasicMove([x,y], [i,j], {c:color,p:p})); + }); + } + else + { + // All other cases + moves.push(this.getBasicMove([x,y], [i,j])); + } + } + } + return moves; + } + + // Find possible captures from a square: look in every direction! + findCaptures(sq) + { + let moves = []; + + Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.PAWN)); + Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.ROOK)); + Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.KNIGHT)); + Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.BISHOP)); + Array.prototype.push.apply(moves, this.findCaptures_aux(sq, V.QUEEN)); + + return moves; + } + + getPotentialPawnMoves([x,y]) + { + const color = this.getColor(x,y); + let moves = []; + const [sizeX,sizeY] = [V.size.x,V.size.y]; + const shift = (color == 'w' ? -1 : 1); + const startRank = (color == 'w' ? sizeY-2 : 1); + const firstRank = (color == 'w' ? sizeY-1 : 0); + const lastRank = (color == "w" ? 0 : sizeY-1); + + if (x+shift != lastRank) + { + // Normal moves + if (this.board[x+shift][y] == V.EMPTY) + { + moves.push(this.getBasicMove([x,y], [x+shift,y])); + if ([startRank,firstRank].includes(x) && this.board[x+2*shift][y] == V.EMPTY) + { + //two squares jump + moves.push(this.getBasicMove([x,y], [x+2*shift,y])); + } + } + } + + else //promotion + { + let promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]; + promotionPieces.forEach(p => { + // Normal move + if (this.board[x+shift][y] == V.EMPTY) + moves.push(this.getBasicMove([x,y], [x+shift,y], {c:color,p:p})); + }); + } + + // No en passant here + + // Add "zen" captures + Array.prototype.push.apply(moves, this.findCaptures([x,y])); + + return moves; + } + + getPotentialRookMoves(sq) + { + let noCaptures = this.getSlideNJumpMoves(sq, V.steps[V.ROOK]); + let captures = this.findCaptures(sq); + return noCaptures.concat(captures); + } + + getPotentialKnightMoves(sq) + { + let noCaptures = this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep"); + let captures = this.findCaptures(sq); + return noCaptures.concat(captures); + } + + getPotentialBishopMoves(sq) + { + let noCaptures = this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]); + let captures = this.findCaptures(sq); + return noCaptures.concat(captures); + } + + getPotentialQueenMoves(sq) + { + let noCaptures = this.getSlideNJumpMoves( + sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP])); + let captures = this.findCaptures(sq); + return noCaptures.concat(captures); + } + + getPotentialKingMoves(sq) + { + // Initialize with normal moves + let noCaptures = this.getSlideNJumpMoves(sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + let captures = this.findCaptures(sq); + return noCaptures.concat(captures).concat(this.getCastleMoves(sq)); + } + + getNotation(move) + { + // Recognize special moves first + if (move.appear.length == 2) + { + // castle + if (move.end.y < move.start.y) + return "0-0-0"; + else + return "0-0"; + } + + // Translate initial square (because pieces may fly unusually in this variant!) + const initialSquare = V.CoordsToSquare(move.start); + + // Translate final square + const finalSquare = V.CoordsToSquare(move.end); + + let notation = ""; + const piece = this.getPiece(move.start.x, move.start.y); + if (piece == V.PAWN) + { + // pawn move (TODO: enPassant indication) + if (move.vanish.length > 1) + { + // capture + notation = initialSquare + "x" + finalSquare; + } + else //no capture + notation = finalSquare; + if (piece != move.appear[0].p) //promotion + notation += "=" + move.appear[0].p.toUpperCase(); + } + + else + { + // Piece movement + notation = piece.toUpperCase(); + if (move.vanish.length > 1) + notation += initialSquare + "x"; + notation += finalSquare; + } + return notation; + } + + static get VALUES() + { + // TODO: experimental + return { + 'p': 1, + 'r': 3, + 'n': 2, + 'b': 2, + 'q': 5, + 'k': 1000 + } + } } diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index a2ce34d9..2180509c 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -466,7 +466,7 @@ export default { }, newChallenge: async function() { if (this.newchallenge.vid == "") - return alert("Please select a variant"); + return alert("Please select a variant"); const vname = this.getVname(this.newchallenge.vid); const vModule = await import("@/variants/" + vname + ".js"); window.V = vModule.VariantRules; diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue index c24e7203..0973d32c 100644 --- a/client/src/views/MyGames.vue +++ b/client/src/views/MyGames.vue @@ -25,7 +25,7 @@ export default { data: function() { return { st: store.state, - display: "live", + display: "live", games: [], }; }, diff --git a/server/app.js b/server/app.js index f97d925b..1fc03c59 100644 --- a/server/app.js +++ b/server/app.js @@ -12,17 +12,17 @@ app.use(favicon(path.join(__dirname, "static", "favicon.ico"))); if (app.get('env') === 'development') { - // Full logging in development mode - app.use(logger('dev')); + // Full logging in development mode + app.use(logger('dev')); } else { - // http://dev.rdybarra.com/2016/06/23/Production-Logging-With-Morgan-In-Express/ - app.set('trust proxy', true); - // In prod, only log error responses (https://github.com/expressjs/morgan) - app.use(logger('combined', { - skip: function (req, res) { return res.statusCode < 400 } - })); + // http://dev.rdybarra.com/2016/06/23/Production-Logging-With-Morgan-In-Express/ + app.set('trust proxy', true); + // In prod, only log error responses (https://github.com/expressjs/morgan) + app.use(logger('combined', { + skip: function (req, res) { return res.statusCode < 400 } + })); } app.use(express.json()); @@ -33,14 +33,14 @@ app.use(express.static(path.join(__dirname, 'static'))); //client "prod" files // In development stage the client side has its own server if (params.cors.enable) { - app.use(function(req, res, next) { - res.header("Access-Control-Allow-Origin", params.cors.allowedOrigin); - res.header("Access-Control-Allow-Credentials", true); //for cookies + app.use(function(req, res, next) { + res.header("Access-Control-Allow-Origin", params.cors.allowedOrigin); + res.header("Access-Control-Allow-Credentials", true); //for cookies res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); - res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE"); + res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE"); next(); - }); + }); } // Routing (AJAX-only) @@ -60,11 +60,11 @@ app.use(function(err, req, res, next) { // render the error page res.status(err.status || 500); res.send(` - <!doctype html> - <h1>= message</h1> - <h2>= error.status</h2> - <pre>#{error.stack}</pre> - `); + <!doctype html> + <h1>= message</h1> + <h2>= error.status</h2> + <pre>#{error.stack}</pre> + `); }); module.exports = app; diff --git a/server/bin/www b/server/bin/www index 44751991..b3a2c732 100755 --- a/server/bin/www +++ b/server/bin/www @@ -30,10 +30,10 @@ var UserModel = require("../models/User"); var ChallengeModel = require("../models/Challenge"); var GameModel = require("../models/Game"); cron.schedule('0 0 0 * * *', function() { - // Remove some old users, challenges and games every 24h - UserModel.cleanUsersDb(); - ChallengeModel.removeOld(); - GameModel.cleanGamesDb(); + // Remove some old users, challenges and games every 24h + UserModel.cleanUsersDb(); + ChallengeModel.removeOld(); + GameModel.cleanGamesDb(); }); /** diff --git a/server/config/parameters.js.dist b/server/config/parameters.js.dist index 5b4301a6..264fac50 100644 --- a/server/config/parameters.js.dist +++ b/server/config/parameters.js.dist @@ -1,34 +1,34 @@ module.exports = { - // For mail sending. NOTE: *no trailing slash* - siteURL: "http://localhost:8080", + // For mail sending. NOTE: *no trailing slash* + siteURL: "http://localhost:8080", - // To know in which environment the code run - env: process.env.NODE_ENV || 'development', - - // CORS: cross-origin resource sharing options - cors: { - enable: true, - allowedOrigin: "http://localhost:8080", - }, + // To know in which environment the code run + env: process.env.NODE_ENV || 'development', + + // CORS: cross-origin resource sharing options + cors: { + enable: true, + allowedOrigin: "http://localhost:8080", + }, - // Lifespan of a (login) cookie - cookieExpire: 183*24*60*60*1000, //6 months in milliseconds + // Lifespan of a (login) cookie + cookieExpire: 183*24*60*60*1000, //6 months in milliseconds - // Characters in a login token, and period of validity (in milliseconds) - token: { - length: 16, - expire: 30*60*1000, //30 minutes in milliseconds - }, + // Characters in a login token, and period of validity (in milliseconds) + token: { + length: 16, + expire: 30*60*1000, //30 minutes in milliseconds + }, - // Email settings - mail: { - host: "mail_host_address", - port: 465, //if secure; otherwise use 587 - secure: true, //...or false - user: "mail_user_name", - pass: "mail_password", - noreply: "some_noreply_email", - contact: "some_contact_email", - }, + // Email settings + mail: { + host: "mail_host_address", + port: 465, //if secure; otherwise use 587 + secure: true, //...or false + user: "mail_user_name", + pass: "mail_password", + noreply: "some_noreply_email", + contact: "some_contact_email", + }, }; diff --git a/server/db/populate.sql b/server/db/populate.sql index 4659704f..b770c575 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -1,21 +1,21 @@ -- Re-run this script after variants are added insert or ignore into Variants (name,description) values - ('Alice', 'Both sides of the mirror'), - ('Antiking', 'Keep antiking in check'), - ('Atomic', 'Explosive captures'), - ('Baroque', 'Exotic captures'), - ('Berolina', 'Pawns move diagonally'), - ('Checkered', 'Shared pieces'), - ('Chess960', 'Standard rules'), - ('Crazyhouse', 'Captures reborn'), - ('Dark', 'In the shadow'), - ('Extinction', 'Capture all of a kind'), - ('Grand', 'Big board'), - ('Losers', 'Lose all pieces'), - ('Magnetic', 'Laws of attraction'), - ('Marseille', 'Move twice'), - ('Switching', 'Exchange pieces positions'), - ('Upsidedown', 'Head upside down'), - ('Wildebeest', 'Balanced sliders & leapers'), - ('Zen', 'Reverse captures'); + ('Alice', 'Both sides of the mirror'), + ('Antiking', 'Keep antiking in check'), + ('Atomic', 'Explosive captures'), + ('Baroque', 'Exotic captures'), + ('Berolina', 'Pawns move diagonally'), + ('Checkered', 'Shared pieces'), + ('Chess960', 'Standard rules'), + ('Crazyhouse', 'Captures reborn'), + ('Dark', 'In the shadow'), + ('Extinction', 'Capture all of a kind'), + ('Grand', 'Big board'), + ('Losers', 'Lose all pieces'), + ('Magnetic', 'Laws of attraction'), + ('Marseille', 'Move twice'), + ('Switching', 'Exchange pieces positions'), + ('Upsidedown', 'Head upside down'), + ('Wildebeest', 'Balanced sliders & leapers'), + ('Zen', 'Reverse captures'); diff --git a/server/gulpfile.js b/server/gulpfile.js index ae739d96..99c58ab3 100644 --- a/server/gulpfile.js +++ b/server/gulpfile.js @@ -2,16 +2,16 @@ var gulp = require('gulp'); var nodemon = require('gulp-nodemon'); //reload server on changes var nodemonOptions = { - script: 'bin/www', - ext: 'js', - env: { 'NODE_ENV': 'development' }, - verbose: true, - watch: ['./','routes','bin'] + script: 'bin/www', + ext: 'js', + env: { 'NODE_ENV': 'development' }, + verbose: true, + watch: ['./','routes','bin'] }; gulp.task('start', function () { - nodemon(nodemonOptions) - .on('restart', function () { - console.log('restarted!') - }); + nodemon(nodemonOptions) + .on('restart', function () { + console.log('restarted!') + }); }); diff --git a/server/models/Game.js b/server/models/Game.js index 5e1c7990..da59b4a6 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -49,11 +49,11 @@ const GameModel = return ""; }, - create: function(vid, fen, timeControl, players, cb) - { - db.serialize(function() { - let query = - "INSERT INTO Games" + create: function(vid, fen, timeControl, players, cb) + { + db.serialize(function() { + let query = + "INSERT INTO Games" + " (vid, fenStart, fen, score, timeControl, created, drawOffer)" + " VALUES (" + vid + ",'" + fen + "','" + fen + "','*','" + timeControl + "'," + Date.now() + "," + false + ")"; @@ -68,51 +68,51 @@ const GameModel = db.run(query); }); cb(null, {gid: this.lastID}); - }); - }); - }, + }); + }); + }, - // TODO: queries here could be async, and wait for all to complete - getOne: function(id, cb) - { - db.serialize(function() { + // TODO: queries here could be async, and wait for all to complete + getOne: function(id, cb) + { + db.serialize(function() { // TODO: optimize queries? - let query = + let query = // NOTE: g.scoreMsg can be NULL // (in this case score = "*" and no reason to look at it) - "SELECT g.id, g.vid, g.fen, g.fenStart, g.timeControl, g.score, " + + "SELECT g.id, g.vid, g.fen, g.fenStart, g.timeControl, g.score, " + "g.scoreMsg, v.name AS vname " + - "FROM Games g " + + "FROM Games g " + "JOIN Variants v " + " ON g.vid = v.id " + - "WHERE g.id = " + id; - db.get(query, (err,gameInfo) => { - if (!!err) - return cb(err); - query = - "SELECT p.uid, p.color, u.name " + - "FROM Players p " + + "WHERE g.id = " + id; + db.get(query, (err,gameInfo) => { + if (!!err) + return cb(err); + query = + "SELECT p.uid, p.color, u.name " + + "FROM Players p " + "JOIN Users u " + " ON p.uid = u.id " + - "WHERE p.gid = " + id; - db.all(query, (err2,players) => { - if (!!err2) - return cb(err2); - query = - "SELECT squares, played, idx " + - "FROM Moves " + - "WHERE gid = " + id; - db.all(query, (err3,moves) => { - if (!!err3) - return cb(err3); - query = + "WHERE p.gid = " + id; + db.all(query, (err2,players) => { + if (!!err2) + return cb(err2); + query = + "SELECT squares, played, idx " + + "FROM Moves " + + "WHERE gid = " + id; + db.all(query, (err3,moves) => { + if (!!err3) + return cb(err3); + query = "SELECT msg, name, added " + "FROM Chats " + "WHERE gid = " + id; - db.all(query, (err4,chats) => { - if (!!err4) - return cb(err4); - const game = Object.assign({}, + db.all(query, (err4,chats) => { + if (!!err4) + return cb(err4); + const game = Object.assign({}, gameInfo, { players: players, @@ -120,41 +120,41 @@ const GameModel = chats: chats, } ); - return cb(null, game); + return cb(null, game); }); - }); - }); - }); - }); - }, + }); + }); + }); + }); + }, - getByUser: function(uid, excluded, cb) - { - db.serialize(function() { - // Next query is fine because a player appear at most once in a game - const query = - "SELECT gid " + - "FROM Players " + - "WHERE uid " + (excluded ? "<>" : "=") + " " + uid; - db.all(query, (err,gameIds) => { - if (!!err) - return cb(err); + getByUser: function(uid, excluded, cb) + { + db.serialize(function() { + // Next query is fine because a player appear at most once in a game + const query = + "SELECT gid " + + "FROM Players " + + "WHERE uid " + (excluded ? "<>" : "=") + " " + uid; + db.all(query, (err,gameIds) => { + if (!!err) + return cb(err); gameIds = gameIds || []; //might be empty - let gameArray = []; - for (let i=0; i<gameIds.length; i++) - { - GameModel.getOne(gameIds[i]["gid"], (err2,game) => { - if (!!err2) - return cb(err2); - gameArray.push(game); - // Call callback function only when gameArray is complete: - if (i == gameIds.length - 1) - return cb(null, gameArray); - }); - } - }); - }); - }, + let gameArray = []; + for (let i=0; i<gameIds.length; i++) + { + GameModel.getOne(gameIds[i]["gid"], (err2,game) => { + if (!!err2) + return cb(err2); + gameArray.push(game); + // Call callback function only when gameArray is complete: + if (i == gameIds.length - 1) + return cb(null, gameArray); + }); + } + }); + }); + }, getPlayers: function(id, cb) { @@ -193,7 +193,7 @@ const GameModel = // obj can have fields move, chat, fen, drawOffer and/or score update: function(id, obj) { - db.parallelize(function() { + db.parallelize(function() { let query = "UPDATE Games " + "SET "; @@ -224,35 +224,35 @@ const GameModel = } if (!!obj.chat) { - query = - "INSERT INTO Chats (gid, msg, name, added) VALUES (" + query = + "INSERT INTO Chats (gid, msg, name, added) VALUES (" + id + ",?,'" + obj.chat.name + "'," + Date.now() + ")"; db.run(query, obj.chat.msg); } }); }, - remove: function(id) - { - db.parallelize(function() { - let query = - "DELETE FROM Games " + - "WHERE id = " + id; - db.run(query); - query = - "DELETE FROM Players " + - "WHERE gid = " + id; - db.run(query); - query = - "DELETE FROM Moves " + - "WHERE gid = " + id; - db.run(query); - query = - "DELETE FROM Chats " + - "WHERE gid = " + id; - db.run(query); - }); - }, + remove: function(id) + { + db.parallelize(function() { + let query = + "DELETE FROM Games " + + "WHERE id = " + id; + db.run(query); + query = + "DELETE FROM Players " + + "WHERE gid = " + id; + db.run(query); + query = + "DELETE FROM Moves " + + "WHERE gid = " + id; + db.run(query); + query = + "DELETE FROM Chats " + + "WHERE gid = " + id; + db.run(query); + }); + }, cleanGamesDb: function() { diff --git a/server/models/User.js b/server/models/User.js index c0516156..1a5397db 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -17,53 +17,53 @@ var sendEmail = require('../utils/mailer'); const UserModel = { - checkNameEmail: function(o) - { - if (typeof o.name === "string") - { - if (o.name.length == 0) - return "Empty name"; - if (!o.name.match(/^[\w]+$/)) - return "Bad characters in name"; - } - if (typeof o.email === "string") - { - if (o.email.length == 0) - return "Empty email"; - if (!o.email.match(/^[\w.+-]+@[\w.+-]+$/)) - return "Bad characters in email"; - } + checkNameEmail: function(o) + { + if (typeof o.name === "string") + { + if (o.name.length == 0) + return "Empty name"; + if (!o.name.match(/^[\w]+$/)) + return "Bad characters in name"; + } + if (typeof o.email === "string") + { + if (o.email.length == 0) + return "Empty email"; + if (!o.email.match(/^[\w.+-]+@[\w.+-]+$/)) + return "Bad characters in email"; + } return ""; //NOTE: not required, but more consistent... (?!) - }, + }, - // NOTE: parameters are already cleaned (in controller), thus no sanitization here - create: function(name, email, notify, callback) - { - db.serialize(function() { - const insertQuery = - "INSERT INTO Users " + - "(name, email, notify, created) VALUES " + - "('" + name + "', '" + email + "', " + notify + "," + Date.now() + ")"; - db.run(insertQuery, err => { - if (!!err) - return callback(err); - db.get("SELECT last_insert_rowid() AS rowid", callback); - }); - }); - }, + // NOTE: parameters are already cleaned (in controller), thus no sanitization here + create: function(name, email, notify, callback) + { + db.serialize(function() { + const insertQuery = + "INSERT INTO Users " + + "(name, email, notify, created) VALUES " + + "('" + name + "', '" + email + "', " + notify + "," + Date.now() + ")"; + db.run(insertQuery, err => { + if (!!err) + return callback(err); + db.get("SELECT last_insert_rowid() AS rowid", callback); + }); + }); + }, - // Find one user (by id, name, email, or token) - getOne: function(by, value, cb) - { - const delimiter = (typeof value === "string" ? "'" : ""); - db.serialize(function() { - const query = - "SELECT * " + - "FROM Users " + - "WHERE " + by + " = " + delimiter + value + delimiter; - db.get(query, cb); - }); - }, + // Find one user (by id, name, email, or token) + getOne: function(by, value, cb) + { + const delimiter = (typeof value === "string" ? "'" : ""); + db.serialize(function() { + const query = + "SELECT * " + + "FROM Users " + + "WHERE " + by + " = " + delimiter + value + delimiter; + db.get(query, cb); + }); + }, getByIds: function(ids, cb) { db.serialize(function() { @@ -75,65 +75,65 @@ const UserModel = }); }, - ///////// - // MODIFY + ///////// + // MODIFY - setLoginToken: function(token, uid, cb) - { - db.serialize(function() { - const query = - "UPDATE Users " + - "SET loginToken = '" + token + "', loginTime = " + Date.now() + " " + - "WHERE id = " + uid; - db.run(query, cb); - }); - }, + setLoginToken: function(token, uid, cb) + { + db.serialize(function() { + const query = + "UPDATE Users " + + "SET loginToken = '" + token + "', loginTime = " + Date.now() + " " + + "WHERE id = " + uid; + db.run(query, cb); + }); + }, - // Set session token only if empty (first login) - // NOTE: weaker security (but avoid to re-login everywhere after each logout) - // TODO: option would be to reset all tokens periodically, e.g. every 3 months + // Set session token only if empty (first login) + // NOTE: weaker security (but avoid to re-login everywhere after each logout) + // TODO: option would be to reset all tokens periodically, e.g. every 3 months trySetSessionToken: function(uid, cb) - { - // Also empty the login token to invalidate future attempts - db.serialize(function() { - const querySessionToken = - "SELECT sessionToken " + - "FROM Users " + - "WHERE id = " + uid; - db.get(querySessionToken, (err,ret) => { - if (!!err) - return cb(err); - const token = ret.sessionToken || genToken(params.token.length); - const queryUpdate = - "UPDATE Users " + - "SET loginToken = NULL" + - (!ret.sessionToken ? (", sessionToken = '" + token + "'") : "") + " " + - "WHERE id = " + uid; - db.run(queryUpdate); - cb(null, token); - }); - }); - }, + { + // Also empty the login token to invalidate future attempts + db.serialize(function() { + const querySessionToken = + "SELECT sessionToken " + + "FROM Users " + + "WHERE id = " + uid; + db.get(querySessionToken, (err,ret) => { + if (!!err) + return cb(err); + const token = ret.sessionToken || genToken(params.token.length); + const queryUpdate = + "UPDATE Users " + + "SET loginToken = NULL" + + (!ret.sessionToken ? (", sessionToken = '" + token + "'") : "") + " " + + "WHERE id = " + uid; + db.run(queryUpdate); + cb(null, token); + }); + }); + }, - updateSettings: function(user, cb) - { - db.serialize(function() { - const query = - "UPDATE Users " + - "SET name = '" + user.name + "'" + - ", email = '" + user.email + "'" + - ", notify = " + user.notify + " " + - "WHERE id = " + user.id; - db.run(query, cb); - }); - }, + updateSettings: function(user, cb) + { + db.serialize(function() { + const query = + "UPDATE Users " + + "SET name = '" + user.name + "'" + + ", email = '" + user.email + "'" + + ", notify = " + user.notify + " " + + "WHERE id = " + user.id; + db.run(query, cb); + }); + }, ///////////////// // NOTIFICATIONS tryNotify: function(oppId, message) { - UserModel.getOne("id", oppId, (err,opp) => { + UserModel.getOne("id", oppId, (err,opp) => { if (!err || !opp.notify) return; //error is ignored here (TODO: should be logged) const subject = "vchess.club - notification"; diff --git a/server/models/Variant.js b/server/models/Variant.js index 20bce4a9..dccc89b5 100644 --- a/server/models/Variant.js +++ b/server/models/Variant.js @@ -9,17 +9,17 @@ var db = require("../utils/database"); const VariantModel = { - getAll: function(callback) - { - db.serialize(function() { - const query = - "SELECT * " + - "FROM Variants"; - db.all(query, callback); - }); - }, + getAll: function(callback) + { + db.serialize(function() { + const query = + "SELECT * " + + "FROM Variants"; + db.all(query, callback); + }); + }, - //create, update, delete: directly in DB + //create, update, delete: directly in DB } module.exports = VariantModel; diff --git a/server/routes/games.js b/server/routes/games.js index e9d9ab46..24bfc82c 100644 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -9,10 +9,10 @@ var params = require("../config/parameters"); // From main hall, start game between players 0 and 1 router.post("/games", access.logged, access.ajax, (req,res) => { const gameInfo = req.body.gameInfo; - if (!Array.isArray(gameInfo.players) || + if (!Array.isArray(gameInfo.players) || !gameInfo.players.some(p => p.id == req.userId)) { - return res.json({errmsg: "Cannot start someone else's game"}); + return res.json({errmsg: "Cannot start someone else's game"}); } const cid = req.body.cid; // Check all entries of gameInfo + cid: @@ -25,29 +25,29 @@ router.post("/games", access.logged, access.ajax, (req,res) => { if (!!error) return res.json({errmsg:error}); ChallengeModel.remove(cid); - GameModel.create( + GameModel.create( gameInfo.vid, gameInfo.fen, gameInfo.timeControl, gameInfo.players, - (err,ret) => { - access.checkRequest(res, err, ret, "Cannot create game", () => { + (err,ret) => { + access.checkRequest(res, err, ret, "Cannot create game", () => { const oppIdx = (gameInfo.players[0].id == req.userId ? 1 : 0); const oppId = gameInfo.players[oppIdx].id; UserModel.tryNotify(oppId, "New game: " + params.siteURL + "/game/" + ret.gid); - res.json({gameId: ret.gid}); - }); - } - ); + res.json({gameId: ret.gid}); + }); + } + ); }); router.get("/games", access.ajax, (req,res) => { - const gameId = req.query["gid"]; - if (!!gameId) + const gameId = req.query["gid"]; + if (!!gameId) { GameModel.getOne(gameId, (err,game) => { - access.checkRequest(res, err, game, "Game not found", () => { + access.checkRequest(res, err, game, "Game not found", () => { res.json({game: game}); - }); - }); + }); + }); } else { @@ -55,10 +55,10 @@ router.get("/games", access.ajax, (req,res) => { const userId = req.query["uid"]; const excluded = !!req.query["excluded"]; GameModel.getByUser(userId, excluded, (err,games) => { - if (!!err) + if (!!err) return res.json({errmsg: err.errmsg || err.toString()}); - res.json({games: games}); - }); + res.json({games: games}); + }); } }); @@ -70,11 +70,11 @@ router.put("/games", access.logged, access.ajax, (req,res) => { if (!gid.toString().match(/^[0-9]+$/)) error = "Wrong game ID"; const obj = req.body.newObj; - error = GameModel.checkGameUpdate(obj); + error = GameModel.checkGameUpdate(obj); if (!!error) return res.json({errmsg: error}); - GameModel.update(gid, obj, (err) => { - if (!!err) + GameModel.update(gid, obj, (err) => { + if (!!err) return res.json(err); // Notify opponent if he enabled notifications: GameModel.getPlayers(gid, (err2,players) => { @@ -85,7 +85,7 @@ router.put("/games", access.logged, access.ajax, (req,res) => { "New move in game: " + params.siteURL + "/game/" + gid); }); res.json({}); - }); + }); }); module.exports = router; diff --git a/server/routes/messages.js b/server/routes/messages.js index b3c158eb..cd93b9fe 100644 --- a/server/routes/messages.js +++ b/server/routes/messages.js @@ -6,19 +6,19 @@ const params = require(__dirname.replace("/routes", "/config/parameters")); // Send a message through contact form router.post("/messages", (req,res,next) => { - if (!req.xhr) - return res.json({errmsg: "Unauthorized access"}); + if (!req.xhr) + return res.json({errmsg: "Unauthorized access"}); const from = req.body["email"]; - const subject = req.body["subject"]; - const body = req.body["content"]; + const subject = req.body["subject"]; + const body = req.body["content"]; - // TODO: sanitize ? - mailer(from, params.mail.contact, subject, body, err => { - if (!!err) - return res.json({errmsg:err}); - // OK, everything fine - res.json({}); //ignored - }); + // TODO: sanitize ? + mailer(from, params.mail.contact, subject, body, err => { + if (!!err) + return res.json({errmsg:err}); + // OK, everything fine + res.json({}); //ignored + }); }); module.exports = router; diff --git a/server/routes/users.js b/server/routes/users.js index 1d553dba..129cda23 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -18,7 +18,7 @@ router.get("/whoami", access.ajax, (req,res) => { }); }; const anonymous = {name:"", email:"", id:0, notify:false}; - if (!req.cookies.token) + if (!req.cookies.token) return callback(anonymous); UserModel.getOne("sessionToken", req.cookies.token, function(err, user) { if (!!err || !user) @@ -41,101 +41,101 @@ router.get("/users", access.ajax, (req,res) => { // to: object user (to who we send an email) function setAndSendLoginToken(subject, to, res) { - // Set login token and send welcome(back) email with auth link - const token = genToken(params.token.length); - UserModel.setLoginToken(token, to.id, err => { - if (!!err) - return res.json({errmsg: err.toString()}); - const body = - "Hello " + to.name + "!\\n" + - "Access your account here: " + - params.siteURL + "/#/authenticate/" + token + "\\n" + - "Token will expire in " + params.token.expire/(1000*60) + " minutes." - sendEmail(params.mail.noreply, to.email, subject, body, err => { - res.json(err || {}); - }); - }); + // Set login token and send welcome(back) email with auth link + const token = genToken(params.token.length); + UserModel.setLoginToken(token, to.id, err => { + if (!!err) + return res.json({errmsg: err.toString()}); + const body = + "Hello " + to.name + "!\\n" + + "Access your account here: " + + params.siteURL + "/#/authenticate/" + token + "\\n" + + "Token will expire in " + params.token.expire/(1000*60) + " minutes." + sendEmail(params.mail.noreply, to.email, subject, body, err => { + res.json(err || {}); + }); + }); } router.post('/register', access.unlogged, access.ajax, (req,res) => { - const name = req.body.name; - const email = req.body.email; - const notify = !!req.body.notify; - const error = UserModel.checkNameEmail({name: name, email: email}); - if (!!error) - return res.json({errmsg: error}); - UserModel.create(name, email, notify, (err,uid) => { - if (!!err) - return res.json({errmsg: err.toString()}); - const user = { - id: uid["rowid"], - name: name, - email: email, - }; - setAndSendLoginToken("Welcome to " + params.siteURL, user, res); - }); + const name = req.body.name; + const email = req.body.email; + const notify = !!req.body.notify; + const error = UserModel.checkNameEmail({name: name, email: email}); + if (!!error) + return res.json({errmsg: error}); + UserModel.create(name, email, notify, (err,uid) => { + if (!!err) + return res.json({errmsg: err.toString()}); + const user = { + id: uid["rowid"], + name: name, + email: email, + }; + setAndSendLoginToken("Welcome to " + params.siteURL, user, res); + }); }); router.get('/sendtoken', access.unlogged, access.ajax, (req,res) => { - const nameOrEmail = decodeURIComponent(req.query.nameOrEmail); - const type = (nameOrEmail.indexOf('@') >= 0 ? "email" : "name"); - const error = UserModel.checkNameEmail({[type]: nameOrEmail}); - if (!!error) - return res.json({errmsg: error}); - UserModel.getOne(type, nameOrEmail, (err,user) => { - access.checkRequest(res, err, user, "Unknown user", () => { - setAndSendLoginToken("Token for " + params.siteURL, user, res); - }); - }); + const nameOrEmail = decodeURIComponent(req.query.nameOrEmail); + const type = (nameOrEmail.indexOf('@') >= 0 ? "email" : "name"); + const error = UserModel.checkNameEmail({[type]: nameOrEmail}); + if (!!error) + return res.json({errmsg: error}); + UserModel.getOne(type, nameOrEmail, (err,user) => { + access.checkRequest(res, err, user, "Unknown user", () => { + setAndSendLoginToken("Token for " + params.siteURL, user, res); + }); + }); }); router.get('/authenticate', access.unlogged, access.ajax, (req,res) => { UserModel.getOne("loginToken", req.query.token, (err,user) => { - access.checkRequest(res, err, user, "Invalid token", () => { + access.checkRequest(res, err, user, "Invalid token", () => { // If token older than params.tokenExpire, do nothing - if (Date.now() > user.loginTime + params.token.expire) - return res.json({errmsg: "Token expired"}); - // Generate session token (if not exists) + destroy login token - UserModel.trySetSessionToken(user.id, (err,token) => { - if (!!err) - return res.json({errmsg: err.toString()}); - // Set cookie + if (Date.now() > user.loginTime + params.token.expire) + return res.json({errmsg: "Token expired"}); + // Generate session token (if not exists) + destroy login token + UserModel.trySetSessionToken(user.id, (err,token) => { + if (!!err) + return res.json({errmsg: err.toString()}); + // Set cookie res.cookie("token", token, { - httpOnly: true, - secure: !!params.siteURL.match(/^https/), - maxAge: params.cookieExpire, - }); - res.json({ + httpOnly: true, + secure: !!params.siteURL.match(/^https/), + maxAge: params.cookieExpire, + }); + res.json({ id: user.id, name: user.name, email: user.email, notify: user.notify, }); - }); - }); - }); + }); + }); + }); }); router.put('/update', access.logged, access.ajax, (req,res) => { - const name = req.body.name; - const email = req.body.email; - const error = UserModel.checkNameEmail({name: name, email: email}); - if (!!error) - return res.json({errmsg: error}); - const user = { - id: req.userId, - name: name, - email: email, - notify: !!req.body.notify, - }; - UserModel.updateSettings(user, err => { - res.json(err ? {errmsg: err.toString()} : {}); - }); + const name = req.body.name; + const email = req.body.email; + const error = UserModel.checkNameEmail({name: name, email: email}); + if (!!error) + return res.json({errmsg: error}); + const user = { + id: req.userId, + name: name, + email: email, + notify: !!req.body.notify, + }; + UserModel.updateSettings(user, err => { + res.json(err ? {errmsg: err.toString()} : {}); + }); }); router.get('/logout', access.logged, access.ajax, (req,res) => { - res.clearCookie("token"); - res.json({}); + res.clearCookie("token"); + res.json({}); }); module.exports = router; diff --git a/server/routes/variants.js b/server/routes/variants.js index f52c6a49..8153c7a7 100644 --- a/server/routes/variants.js +++ b/server/routes/variants.js @@ -6,11 +6,11 @@ const VariantModel = require("../models/Variant"); const access = require("../utils/access"); router.get('/variants', access.ajax, function(req, res, next) { - VariantModel.getAll((err,variants) => { - if (!!err) - return next(err); - res.json({variantArray:variants}); - }); + VariantModel.getAll((err,variants) => { + if (!!err) + return next(err); + res.json({variantArray:variants}); + }); }); module.exports = router; diff --git a/server/utils/access.js b/server/utils/access.js index 2e2fa92d..d51c4b77 100644 --- a/server/utils/access.js +++ b/server/utils/access.js @@ -2,65 +2,65 @@ var UserModel = require("../models/User"); module.exports = { - // Prevent access to "users pages" - logged: function(req, res, next) { - const callback = () => { - if (!loggedIn) - return res.json({errmsg: "Not logged in"}); - next(); - }; - let loggedIn = undefined; - if (!req.cookies.token) - { - loggedIn = false; - callback(); - } - else - { - UserModel.getOne("sessionToken", req.cookies.token, function(err, user) { - if (!!user) - { - req.userId = user.id; - req.userName = user.name; - loggedIn = true; - } - else - { - // Token in cookies presumably wrong: erase it - res.clearCookie("token"); - loggedIn = false; - } - callback(); - }); - } - }, + // Prevent access to "users pages" + logged: function(req, res, next) { + const callback = () => { + if (!loggedIn) + return res.json({errmsg: "Not logged in"}); + next(); + }; + let loggedIn = undefined; + if (!req.cookies.token) + { + loggedIn = false; + callback(); + } + else + { + UserModel.getOne("sessionToken", req.cookies.token, function(err, user) { + if (!!user) + { + req.userId = user.id; + req.userName = user.name; + loggedIn = true; + } + else + { + // Token in cookies presumably wrong: erase it + res.clearCookie("token"); + loggedIn = false; + } + callback(); + }); + } + }, - // Prevent access to "anonymous pages" - unlogged: function(req, res, next) { - // Just a quick heuristic, which should be enough - const loggedIn = !!req.cookies.token; - if (loggedIn) - return res.json({errmsg: "Already logged in"}); - next(); - }, + // Prevent access to "anonymous pages" + unlogged: function(req, res, next) { + // Just a quick heuristic, which should be enough + const loggedIn = !!req.cookies.token; + if (loggedIn) + return res.json({errmsg: "Already logged in"}); + next(); + }, - // Prevent direct access to AJAX results - ajax: function(req, res, next) { + // Prevent direct access to AJAX results + ajax: function(req, res, next) { if (!req.xhr) - return res.json({errmsg: "Unauthorized access"}); - next(); - }, + return res.json({errmsg: "Unauthorized access"}); + next(); + }, - // Check for errors before callback (continue page loading). TODO: better name. - checkRequest: function(res, err, out, msg, cb) { - if (!!err) - return res.json({errmsg: err.errmsg || err.toString()}); - if (!out - || (Array.isArray(out) && out.length == 0) - || (typeof out === "object" && Object.keys(out).length == 0)) - { - return res.json({errmsg: msg}); - } - cb(); - }, + // Check for errors before callback (continue page loading). TODO: better name. + checkRequest: function(res, err, out, msg, cb) { + if (!!err) + return res.json({errmsg: err.errmsg || err.toString()}); + if (!out + || (Array.isArray(out) && out.length == 0) + || (typeof out === "object" && Object.keys(out).length == 0)) + { + return res.json({errmsg: msg}); + } + cb(); + }, } diff --git a/server/utils/database.js b/server/utils/database.js index ae7c7a66..2904f2ae 100644 --- a/server/utils/database.js +++ b/server/utils/database.js @@ -2,7 +2,7 @@ const sqlite3 = require('sqlite3'); const params = require("../config/parameters") if (params.env == "development") - sqlite3.verbose(); + sqlite3.verbose(); const DbPath = __dirname.replace("/utils", "/db/vchess.sqlite"); const db = new sqlite3.Database(DbPath); diff --git a/server/utils/mailer.js b/server/utils/mailer.js index 86e647c2..9a42a174 100644 --- a/server/utils/mailer.js +++ b/server/utils/mailer.js @@ -3,42 +3,42 @@ const params = require("../config/parameters"); module.exports = function(from, to, subject, body, cb) { - // Avoid the actual sending in development mode - if (params.env === 'development') - { - console.log("New mail: from " + from + " / to " + to); - console.log("Subject: " + subject); - let msgText = body.split('\\n'); - msgText.forEach(msg => { console.log(msg); }); - return cb(); - } + // Avoid the actual sending in development mode + if (params.env === 'development') + { + console.log("New mail: from " + from + " / to " + to); + console.log("Subject: " + subject); + let msgText = body.split('\\n'); + msgText.forEach(msg => { console.log(msg); }); + return cb(); + } // Create reusable transporter object using the default SMTP transport - const transporter = nodemailer.createTransport({ - host: params.mail.host, - port: params.mail.port, - secure: params.mail.secure, - auth: { - user: params.mail.user, - pass: params.mail.pass - } - }); + const transporter = nodemailer.createTransport({ + host: params.mail.host, + port: params.mail.port, + secure: params.mail.secure, + auth: { + user: params.mail.user, + pass: params.mail.pass + } + }); - // Setup email data with unicode symbols - const mailOptions = { - from: params.mail.noreply, - to: to, - subject: subject, - text: body, + // Setup email data with unicode symbols + const mailOptions = { + from: params.mail.noreply, + to: to, + subject: subject, + text: body, replyTo: from, }; - // Send mail with the defined transport object - transporter.sendMail(mailOptions, (error, info) => { - if (!!error) - return cb(error); + // Send mail with the defined transport object + transporter.sendMail(mailOptions, (error, info) => { + if (!!error) + return cb(error); // Ignore info. Option: - //console.log('Message sent: %s', info.messageId); - return cb(); + //console.log('Message sent: %s', info.messageId); + return cb(); }); } diff --git a/server/utils/tokenGenerator.js b/server/utils/tokenGenerator.js index b549198a..858bc3bf 100644 --- a/server/utils/tokenGenerator.js +++ b/server/utils/tokenGenerator.js @@ -1,13 +1,13 @@ function randString() { - return Math.random().toString(36).substr(2); // remove `0.` + return Math.random().toString(36).substr(2); // remove `0.` } module.exports = function(tlen) { - let res = ""; - let nbRands = Math.ceil(tlen/10); //10 = min length of a rand() string - for (let i = 0; i < nbRands; i++) - res += randString(); - return res.substr(0, tlen); + let res = ""; + let nbRands = Math.ceil(tlen/10); //10 = min length of a rand() string + for (let i = 0; i < nbRands; i++) + res += randString(); + return res.substr(0, tlen); } -- 2.44.0