Embedded rules language not updated when language is set (in Analyse, Game and Problems)
If new live game starts in background, "new game" notify OK but not first move.
-NEW VARIANTS, Non-chess ( won't add draughts: https://lidraughts.org/ )
-Gomoku, Konane
-Fanorona https://fr.wikipedia.org/wiki/Fanorona
-Yoté https://fr.wikipedia.org/wiki/Yot%C3%A9 http://www.zillionsofgames.com/cgi-bin/zilligames/submissions.cgi/92187?do=show;id=960
+"FreeBoard", re-using a lot of Board logic, but with SVG (empty) board + SVG (empty) reserves.
+Will be used for variants with custom non-rectangular board (Hex, at least)
+Or, with other board shapes (see greenchess.net for example)
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+ <line x1="25" y1="25" x2="75" y2="75" stroke="black" stroke-width="3"/>
+ <circle cx="25" cy="25" r="10" fill="red" stroke="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+ <line x1="50" y1="25" x2="50" y2="75" stroke="black" stroke-width="3"/>
+ <circle cx="50" cy="25" r="10" fill="red" stroke="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+ <line x1="75" y1="25" x2="25" y2="75" stroke="black" stroke-width="3"/>
+ <circle cx="75" cy="25" r="10" fill="red" stroke="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+ <line x1="25" y1="50" x2="75" y2="50" stroke="black" stroke-width="3"/>
+ <circle cx="25" cy="50" r="10" fill="red" stroke="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+ <line x1="25" y1="50" x2="75" y2="50" stroke="black" stroke-width="3"/>
+ <circle cx="75" cy="50" r="10" fill="red" stroke="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+ <line x1="75" y1="25" x2="25" y2="75" stroke="black" stroke-width="3"/>
+ <circle cx="25" cy="75" r="10" fill="red" stroke="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+ <line x1="50" y1="25" x2="50" y2="75" stroke="black" stroke-width="3"/>
+ <circle cx="50" cy="75" r="10" fill="red" stroke="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="100" height="100">
+ <line x1="25" y1="25" x2="75" y2="75" stroke="black" stroke-width="3"/>
+ <circle cx="75" cy="75" r="10" fill="red" stroke="none"/>
+</svg>
// Stop at the first move found
// TODO: not really, it explores all moves from a square (one is enough).
+ // Possible fix: add extra arg "oneMove" to getPotentialMovesFrom,
+ // and then return only boolean true at first move found
+ // (in all getPotentialXXXMoves() ... for all variants ...)
atLeastOneMove() {
const color = this.turn;
for (let i = 0; i < V.size.x; i++) {
const squareWidth = boardElt.offsetWidth / sizeY;
const offset = [boardElt.offsetTop, boardElt.offsetLeft];
const maxNbeltsPerRow = Math.min(this.choices.length, sizeY);
- let topOffset = offset[0] + (sizeY / 2) * squareWidth - squareWidth / 2;
+ let topOffset = offset[0] + ((sizeX - 1) / 2) * squareWidth;
let choicesHeight = squareWidth;
if (this.choices.length >= sizeY) {
// A second row is required (Eightpieces variant)
p.boxed
- | TODO
+ | Capture stones by approach or withdrawal.
+
+p The following is summarized from Wikipedia.
+
+p.
+ The Fanorona board consists of 5 rows and 9 columns.
+ A stone can only move to an adjacent intersection, following the lines.
+ This movement can lead to captures.
+
+h3 General rules
+
+ul
+ li Players alternate turns, starting with White.
+ li There are two kinds of moves: non-capturing ("paika") and capturing.
+ li.
+ If at the beginning of a turn a capturing move is possible,
+ then it has to be played.
+ li
+ | Capturing implies removing one or more pieces of the opponent, in one of
+ | two ways:
+ ul
+ li.
+ Approach — moving the capturing stone to a point adjacent to an
+ opponent's stone, located right after in the movement's direction.
+ li.
+ Withdrawal — the capturing stone moves away from an opponent's
+ stone, initially adjacent.
+ li.
+ When an opponent stone is captured, all opponent pieces in line beyond
+ that stone (and connected to it) are captured as well.
+ Another capture can then be achieved with the same stone.
+
+figure.diagram-container
+ .diagram.diag12
+ | fen:pppp1p1pp/pppppPppp/pP1PPp1pP/PPPpPPPPP/PPPP1PPPP:
+ .diagram.diag22
+ | fen:pppp1p1pp/pppppP1pp/pP1PPpppP/PPPpPP1PP/PPPP1P1PP:
+ figcaption Before and after g4g3 (capturing g2 and g1).
+
+h3 Some restrictions
+
+ul
+ li.
+ An approach capture and a withdrawal capture cannot be made at the same
+ time. The two locations will appear on the interface and you'll have
+ to make a choice.
+ li
+ | The capturing piece is allowed to continue making successive captures,
+ | with these restrictions:
+ ul
+ li The piece is not allowed to arrive at the same position twice.
+ li.
+ It is not permitted to capture twice consecutively in the same
+ direction (first by withdrawal, and then by approach).
+ | However, continuing the capturing sequence is optional.
+
+p.
+ The game ends when one player captures all stones of the opponent.
+ If neither player can achieve this, then the game is a draw.
p.boxed
- | TODO
+ | Captura piedras por percusión o succión.
+
+p El resto se resume en Wikipedia.
+
+p.
+ El tablero de Fanorona tiene 5 filas y 9 columnas.
+ Una piedra solo puede moverse a una intersección adyacente,
+ siguiendo las líneas. Este movimiento puede dar lugar a capturas.
+
+h3 Reglas generales
+
+ul
+ li Los jugadores alternan turnos, comenzando con las blancas.
+ li Hay dos tipos de jugadas: no capturadoras ("paika") y capturadoras.
+ li.
+ Si al comienzo de un turno es posible un movimiento de captura,
+ entonces debe jugarse.
+ li
+ | Capturar implica eliminar una o más piezas opuestas,
+ | una de las dos formas siguientes:
+ li.
+ Percusión — moviendo la piedra de captura a un punto
+ adyacente a una piedra enemiga, ubicada justo después en la dirección
+ del movimiento.
+ li.
+ Succión — la piedra capturadora se aleja de una piedra enemiga,
+ inicialmente adyacente.
+ li.
+ Cuando se captura una piedra enemiga, todas las demás piezas enemigas
+ en línea más allá de esta piedra (y conectados a ella) están
+ también capturado. Entonces es posible otra captura
+ con la misma piedra.
+
+figure.diagram-container
+ .diagram.diag12
+ | fen:pppp1p1pp/pppppPppp/pP1PPp1pP/PPPpPPPPP/PPPP1PPPP:
+ .diagram.diag22
+ | fen:pppp1p1pp/pppppP1pp/pP1PPpppP/PPPpPP1PP/PPPP1P1PP:
+ figcaption Antes y después de g4g3 (capturando g2 y g1).
+
+h3 Algunas restricciones
+
+ul
+ li.
+ No se puede realizar la captura por succión y percusión
+ al mismo tiempo. Ambas ubicaciones aparecerán en la pantalla y deberá
+ hacer una elección.
+ li
+ | Se permite que la pieza de captura continúe capturando, módulo el
+ | siguientes restricciones:
+ ul
+ li La pieza no se debe planchar dos veces en el mismo lugar.
+ li.
+ No puedes capturar dos veces consecutivas en la misma dirección
+ (primero por succión, luego por percusión).
+ | Sin embargo, continuar con la secuencia de captura es opcional.
+
+p.
+ El juego termina cuando uno de los jugadores ha capturado todos los
+ del otro. Si ninguno de ellos tiene éxito, entonces es un empate.
p.boxed
- | TODO
+ | Capturez des pierres par percussion ou aspiration.
+
+p La suite est résumée depuis Wikipedia.
+
+p.
+ Le plateau de Fanorona comporte 5 rangées et 9 colonnes.
+ Une pierre peut seulement se déplacer vers une intersection adjacente,
+ suivant les lignes. Ce mouvement peut donner lieu à des captures.
+
+h3 Règles générales
+
+ul
+ li Les joueurs alternent les tours, commençant avec les blancs.
+ li Il y a deux types de coups : non-capturants ("paika") et capturants.
+ li.
+ Si au début d'un tour un coup capturant est possible,
+ alors il doit être joué.
+ li
+ | Capturer implique retirer une ou plusieurs pièces adverses,
+ | d'une des deux manières suivantes :
+ li.
+ Percussion — en déplaçant la pierre capturante vers un point
+ adjacent à une pierre ennemie, située juste après dans la direction du
+ mouvement.
+ li.
+ Aspiration — la pierre capturante s'éloigne d'une pierre ennemie,
+ initialement adjacente.
+ li.
+ Quand une pierre ennemie est capturée, toutes les autres pièces adverses
+ en ligne au-delà de cette pierre (et connectées à celle-ci) sont
+ capturées également. Une autre capture est ensuite envisageable
+ avec la même pierre.
+
+figure.diagram-container
+ .diagram.diag12
+ | fen:pppp1p1pp/pppppPppp/pP1PPp1pP/PPPpPPPPP/PPPP1PPPP:
+ .diagram.diag22
+ | fen:pppp1p1pp/pppppP1pp/pP1PPpppP/PPPpPP1PP/PPPP1P1PP:
+ figcaption Avant et après g4g3 (capturant g2 et g1).
+
+h3 Quelques restrictions
+
+ul
+ li.
+ Une capture par percussion et une par aspiration ne peuvent être réalisées
+ en même temps. Les deux emplacements apparaitront à l'écran et vous devrez
+ faire un choix.
+ li
+ | La pièce capturante est autorisée à continuer de capturer, modulo les
+ | restrictions suivantes :
+ ul
+ li La pièce ne doit pas repasser deux fois au même endroit.
+ li.
+ On ne peut pas capturer deux fois consécutives dans la même direction
+ (d'abord par aspiration, puis par percussion).
+ | Cependant, continuer la séquence de capture est optionnel.
+
+p.
+ La partie s'achève quand un des joueurs a capturé toutes les pierres de
+ l'autre. Si aucun des deux n'y parvient, alors c'est un match nul.
setOtherVariables(fen) {
// Local stack of captures during a turn (squares + directions)
- this.captures = [];
+ this.captures = [ [] ];
}
static get size() {
return { x: 5, y: 9 };
}
- static get PIECES() {
- return [V.PAWN];
- }
-
getPiece() {
return V.PAWN;
}
+ static IsGoodPosition(position) {
+ if (position.length == 0) return false;
+ const rows = position.split("/");
+ if (rows.length != V.size.x) return false;
+ for (let row of rows) {
+ let sumElts = 0;
+ for (let i = 0; i < row.length; i++) {
+ if (row[i].toLowerCase() == V.PAWN) sumElts++;
+ else {
+ const num = parseInt(row[i], 10);
+ if (isNaN(num) || num <= 0) return false;
+ sumElts += num;
+ }
+ }
+ if (sumElts != V.size.y) return false;
+ }
+ return true;
+ }
+
getPpath(b) {
return "Fanorona/" + b;
}
- //TODO
- //getPPpath() {}
+ getPPpath(m) {
+ // m.vanish.length >= 2, first capture gives direction
+ const ref = (Math.abs(m.vanish[1].x - m.start.x) == 1 ? m.start : m.end);
+ const step = [m.vanish[1].x - ref.x, m.vanish[1].y - ref.y];
+ const normalizedStep = [
+ step[0] / Math.abs(step[0]),
+ step[1] / Math.abs(step[1])
+ ];
+ return (
+ "Fanorona/arrow_" +
+ (normalizedStep[0] || 0) + "_" + (normalizedStep[1] || 0)
+ );
+ }
+
+ // After moving, add stones captured in "step" direction from new location
+ // [x, y] to mv.vanish (if any captured stone!)
+ addCapture([x, y], step, move) {
+ let [i, j] = [x + step[0], y + step[1]];
+ const oppCol = V.GetOppCol(move.vanish[0].c);
+ while (
+ V.OnBoard(i, j) &&
+ this.board[i][j] != V.EMPTY &&
+ this.getColor(i, j) == oppCol
+ ) {
+ move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: V.PAWN }));
+ i += step[0];
+ j += step[1];
+ }
+ return (move.vanish.length >= 2);
+ }
getPotentialMovesFrom([x, y]) {
- // NOTE: (x + y) % 2 == 0 ==> has diagonals
- // TODO
- // Même stratégie que Yote, revenir sur ses pas si stop avant de tout capturer
- // Mais première capture obligatoire (si this.captures.length == 0).
- // After a capture: allow only capturing.
- // Warning: case 3 on Wikipedia page, if both percussion & aspiration,
- // two different moves, cannot take all ==> adjust getPPpath showing arrows.
- // nice looking arrows, with something representing a capture at its end...
- return [];
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ if (L > 0) {
+ var c = captures[L-1];
+ if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1])
+ return [];
+ }
+ const oppCol = V.GetOppCol(this.turn);
+ let steps = V.steps[V.ROOK];
+ if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+ let moves = [];
+ for (let s of steps) {
+ if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) {
+ // Add a move to say "I'm done capturing"
+ moves.push(
+ new Move({
+ appear: [],
+ vanish: [],
+ start: { x: x, y: y },
+ end: { x: x - s[0], y: y - s[1] }
+ })
+ );
+ continue;
+ }
+ let [i, j] = [x + s[0], y + s[1]];
+ if (captures.some(c => c.square.x == i && c.square.y == j)) continue;
+ if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+ // The move is potentially allowed. Might lead to 2 different captures
+ let mv = super.getBasicMove([x, y], [i, j]);
+ const capt = this.addCapture([i, j], s, mv);
+ if (capt) {
+ moves.push(mv);
+ mv = super.getBasicMove([x, y], [i, j]);
+ }
+ const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv);
+ if (capt_bw) moves.push(mv);
+ // Captures take priority (if available)
+ if (!capt && !capt_bw && L == 0) moves.push(mv);
+ }
+ }
+ return moves;
+ }
+
+ atLeastOneCapture() {
+ const color = this.turn;
+ const oppCol = V.GetOppCol(color);
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ const L = captures.length;
+ if (L > 0) {
+ // If some adjacent enemy stone, with free space to capture it,
+ // toward a square not already visited, through a different step
+ // from last one: then yes.
+ const c = captures[L-1];
+ const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]];
+ let steps = V.steps[V.ROOK];
+ if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+ // TODO: half of the steps explored are redundant
+ for (let s of steps) {
+ if (s[0] == c.step[0] && s[1] == c.step[1]) continue;
+ const [i, j] = [x + s[0], y + s[1]];
+ if (
+ !V.OnBoard(i, j) ||
+ this.board[i][j] != V.EMPTY ||
+ captures.some(c => c.square.x == i && c.square.y == j)
+ ) {
+ continue;
+ }
+ if (
+ V.OnBoard(i + s[0], j + s[1]) &&
+ this.board[i + s[0]][j + s[1]] != V.EMPTY &&
+ this.getColor(i + s[0], j + s[1]) == oppCol
+ ) {
+ return true;
+ }
+ if (
+ V.OnBoard(x - s[0], y - s[1]) &&
+ this.board[x - s[0]][y - s[1]] != V.EMPTY &&
+ this.getColor(x - s[0], y - s[1]) == oppCol
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ 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 &&
+ // TODO: this could be more efficient
+ this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2)
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ static KeepCaptures(moves) {
+ return moves.filter(m => m.vanish.length >= 2);
+ }
+
+ getPossibleMovesFrom(sq) {
+ let moves = this.getPotentialMovesFrom(sq);
+ const L0 = this.captures.length;
+ const captures = this.captures[L0 - 1];
+ if (captures.length > 0) return this.getPotentialMovesFrom(sq);
+ const captureMoves = V.KeepCaptures(moves);
+ if (captureMoves.length > 0) return captureMoves;
+ if (this.atLeastOneCapture()) return [];
+ return moves;
+ }
+
+ getAllValidMoves() {
+ const moves = super.getAllValidMoves();
+ if (moves.some(m => m.vanish.length >= 2)) return V.KeepCaptures(moves);
+ return moves;
}
filterValid(moves) {
return [];
}
- //TODO: function aux to detect if continuation captures
- //(not trivial, but not difficult)
-
play(move) {
const color = this.turn;
move.turn = color; //for undo
- const captureNotEnding = (
- move.vanish.length >= 2 &&
- true //TODO: detect if there are continuation captures
- );
- this.captures.push(captureNotEnding); //TODO: something more structured
- //with square + direction of capture
- if (captureNotEnding) move.notTheEnd = true;
- else {
- this.turn = oppCol;
+ V.PlayOnBoard(this.board, move);
+ const L0 = this.captures.length;
+ let captures = this.captures[L0 - 1];
+ if (move.vanish.length >= 2) {
+ captures.push({
+ square: move.start,
+ step: [move.end.x - move.start.x, move.end.y - move.start.y]
+ });
+ if (this.atLeastOneCapture())
+ // There could be other captures (optional)
+ // This field is mostly useful for computer play.
+ move.notTheEnd = true;
+ else captures.pop(); //useless now
+ }
+ if (!move.notTheEnd) {
+ this.turn = V.GetOppCol(color);
this.movesCount++;
+ this.captures.push([]);
}
- this.postPlay(move);
}
undo(move) {
V.UndoOnBoard(this.board, move);
- this.captures.pop();
if (move.turn != this.turn) {
this.turn = move.turn;
this.movesCount--;
+ this.captures.pop();
+ }
+ else {
+ const L0 = this.captures.length;
+ let captures = this.captures[L0 - 1];
+ captures.pop();
}
- this.postUndo(move);
}
getCurrentScore() {
}
getComputerMove() {
- const moves = super.getAllValidMoves();
+ const moves = this.getAllValidMoves();
if (moves.length == 0) return null;
const color = this.turn;
// Capture available? If yes, play it
if (candidates.length >= 1) mv = candidates[randInt(candidates.length)];
else mv = captures[randInt(captures.length)];
this.play(mv);
- captures = (this.turn == color ? super.getAllValidMoves() : []);
+ mvArray.push(mv);
+ captures = (this.turn == color ? this.getAllValidMoves() : []);
}
if (mvArray.length >= 1) {
for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
return mvArray;
}
- // Just play a random move, which if possible do not let a capture
+ // Just play a random move, which if possible does not let a capture
let candidates = [];
for (let m of moves) {
this.play(m);
- const moves2 = super.getAllValidMoves();
- if (moves2.every(m2 => m2.vanish.length <= 1))
- candidates.push(m);
+ if (!this.atLeastOneCapture()) candidates.push(m);
this.undo(m);
}
if (candidates.length >= 1) return candidates[randInt(candidates.length)];
}
getNotation(move) {
+ if (move.appear.length == 0) return "stop";
return (
V.CoordsToSquare(move.start) +
- (move.vanish.length >= 2 ? "x" : "") +
- V.CoordsToSquare(move.end)
+ V.CoordsToSquare(move.end) +
+ (move.vanish.length >= 2 ? "X" : "")
);
}
return true;
}
- static get PIECES() {
- return V.PAWN;
- }
-
getPiece() {
return V.PAWN;
}
for (let row of rows) {
let sumElts = 0;
for (let i = 0; i < row.length; i++) {
- if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
+ if (row[i].toLowerCase() == V.PAWN) sumElts++;
else {
const num = parseInt(row[i], 10);
if (isNaN(num) || num <= 0) return false;
return true;
}
+ static IsGoodPosition(position) {
+ if (position.length == 0) return false;
+ const rows = position.split("/");
+ if (rows.length != V.size.x) return false;
+ for (let row of rows) {
+ let sumElts = 0;
+ for (let i = 0; i < row.length; i++) {
+ if (row[i].toLowerCase() == V.PAWN) sumElts++;
+ else {
+ const num = parseInt(row[i], 10);
+ if (isNaN(num) || num <= 0) return false;
+ sumElts += num;
+ }
+ }
+ if (sumElts != V.size.y) return false;
+ }
+ return true;
+ }
+
static IsGoodFen(fen) {
if (!ChessRules.IsGoodFen(fen)) return false;
const fenParsed = V.ParseFen(fen);
return { x: 5, y: 6 };
}
- static get PIECES() {
- return [V.PAWN];
- }
-
getColor(i, j) {
if (i >= V.size.x) return i == V.size.x ? "w" : "b";
return this.board[i][j].charAt(0);