setTimeout(() => {
if (this.currentUrl != document.location.href) return; //page change
self.$refs["basegame"].play(compMove, "received");
- self.processMove(compMove);
+ const animationLength =
+ // 250 = length of animation, 500 = delay between sub-moves
+ // TODO: a callback would be cleaner.
+ 250 + (Array.isArray(compMove) ? (compMove.length - 1) * 750 : 0);
+ setTimeout(() => self.processMove(compMove), animationLength);
self.compThink = false;
if (self.game.score != "*")
// User action
p.boxed
- | Move twice at every turn.
+ | Move twice at every turn. The goal is to capture the king.
p.
The only difference with orthodox chess is the double-move rule,
p.
At the very first move of the game, white make only one move - as usual.
However, after that and for all the game each side must play twice at
- every turn. The goal is to checkmate.
+ every turn (even if the first move captures the enemy king).
figure.diagram-container
.diagram.diag12
| fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR:
figcaption.
Left: after the moves 1.e4 e5, the 'd' pawn is pinned.
- Right: the black king is under check.
+ Right: the black king is "under check".
p.
In the diagram position on the left, after the first black move ...e5,
- the 'd' pawn is pinned because moving it would allow 2.Bb5,Bxe8 capturing
- the king.
- On the right, after 2.Qf3,Bc4 black king is under check because of
- the options 3.Bxf7,Bxe8 or 3.Qxf7,Qxe8.
+ moving the 'd' pawn would allow 2.Bb5,Bxe8 capturing the king.
+ On the right, after 2.Qf3,Bc4 the threats are 3.Bxf7,Bxe8 and 3.Qxf7,Qxe8.
h3 En-passant capture
| . It is also playable
a(href="https://greenchess.net/rules.php?v=double-move") on greenchess
| .
+
+p Inventor: Fred Galvin (1957)
p.boxed
- | Juega dos jugadas a cada turno.
+ | Juega dos jugadas a cada turno. El objetivo es capturar al rey.
p.
La única diferencia con el juego ortodoxo es la regla del doble movimiento,
pero eso afecta mucho el juego
p.
- Al comienzo del juego, los blancos solo juegan un movimiento, como
+ Al comienzo del juego, las blancas solo juegan un movimiento, como
en general. Sin embargo, después de eso y por el resto del juego,
- cada lado debe jugar dos jugadas cada turno.
- El objetivo es de dar jaque mate.
+ cada lado debe jugar dos jugadas cada turno
+ (incluso si la primera captura al rey contrario).
figure.diagram-container
.diagram.diag12
| fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR:
figcaption.
Izquierda: después de las jugadas 1.e4 e5, el peón 'd' se clava.
- Derecha: el rey negro está en jaque.
+ Derecha: el rey negro está "en jaque".
p.
En la posición del diagrama de la izquierda, después del primer movimiento
- negro ...e5, el peón 'd' está clavado porque su movimiento autorizaría
- 2.Bb5,Bxe8 captura el rey.
- A la derecha, después de 2.Qf3,Bc4 el rey negro está en jaque debido a las
- posibilidades 3.Bxf7,Bxe8 o 3.Qxf7,Qxe8.
+ negro ...e5, un movimiento del peón 'd' autorizaría 2.Bb5,Bxe8 que
+ captura el rey. A la derecha, después de 2.Qf3,Bc4 las amenazas son
+ 3.Bxf7,Bxe8 y 3.Qxf7,Qxe8.
h3 Captura en passant
| . También es jugable
a(href="https://greenchess.net/rules.php?v=double-move") en greenchess
| .
+
+p Inventor: Fred Galvin (1957)
p.boxed
- | Jouez deux coups à chaque tour.
+ | Jouez deux coups à chaque tour. Le but est de capturer le roi.
p.
La seule différence avec le jeu orthodoxe est la règle du double-coup, mais
p.
Au tout début de la partie les blancs ne jouent qu'un seul coup, comme
d'habitude. Cependant, après cela et ce pour tout le reste de la partie
- chaque camp doit jouer deux coups à chaque tour. L'objectif est de mater.
+ chaque camp doit jouer deux coups à chaque tour
+ (même si le premier capture le roi adverse).
figure.diagram-container
.diagram.diag12
| fen:r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR:
figcaption.
Gauche : après les coups 1.e4 e5, le pion 'd' est cloué.
- Droite : le roi noir est en échec.
+ Droite : le roi noir est "en échec".
p.
Dans la position du diagramme à gauche, après le premier coup noir ...e5,
- le pion 'd' est cloué car son déplacement autoriserait 2.Bb5,Bxe8 capturant
- le roi.
- À droite, après 2.Qf3,Bc4 le roi noir est en échec à cause des possibilités
- 3.Bxf7,Bxe8 ou 3.Qxf7,Qxe8.
+ un déplacement du pion 'd' autoriserait 2.Bb5,Bxe8 capturant le roi.
+ À droite, après 2.Qf3,Bc4 les menaces sont 3.Bxf7,Bxe8 et 3.Qxf7,Qxe8.
h3 Prise en passant
| . Elle est jouable également
a(href="https://greenchess.net/rules.php?v=double-move") sur greenchess
| .
+
+p Inventeur : Fred Galvin (1957)
p.
The white army can appear much too small, but the power to move twice in a
- row shouldn't be underestimated. At each turn white plays two moves with
- only one constraint: do not be under check in the end.
- So if the white king attacks a defended piece, he can take it anyway by
- coming back on its initial square on (sub)move 2.
+ row shouldn't be underestimated. At each turn white plays two moves
+ without any constraint. The goal is to capture the king.
figure.diagram-container
.diagram.diag12
p.
El ejército blanco puede parecer demasiado pequeño, pero el poder de jugar
dos veces seguidas y no debe subestimarse. En cada turno las blancas juegan
- dos jugadas con la única restricción de no estar en jaque hasta el final.
- Entonces, si tu rey blanco ataca una habitación protegida, él puede
- tómalo de todos modos y luego regresa a tu caso inicial en el segundo
- (sub)movimiento.
+ dos jugadas sin ninguna restricción. El objetivo es capturar al rey.
figure.diagram-container
.diagram.diag12
p.
L'armée blanche peut paraître bien trop réduite, mais le pouvoir de jouer
deux fois d'affilée ne doit pas être sous-estimé. À chaque tour les
- blancs jouent deux coups avec pour seule contrainte de ne pas être en échec
- à la fin. Ainsi, si le roi blanc attaque une pièce protégée, il peut la
- prendre quand-même puis revenir sur sa case initiale au second (sous)coup.
+ blancs jouent deux coups sans aucune contrainte.
+ L'objectif est de capturer le roi.
figure.diagram-container
.diagram.diag12
return res;
};
- let moves11 = this.getAllValidMoves();
+ const moves11 = this.getAllValidMoves();
let doubleMoves = [];
// Rank moves using a min-max at depth 2
for (let i = 0; i < moves11.length; i++) {
if (this.turn != color) {
// We gave check with last move: search the best opponent move
doubleMoves.push({ moves: [moves11[i]], eval: getBestMoveEval() });
- } else {
+ }
+ 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()
+ eval: getBestMoveEval() + 0.05 - Math.random() / 10
});
this.undo(moves12[j]);
}
this.undo(moves11[i]);
}
+ // TODO: array + sort + candidates logic not required when adding small
+ // fluctuations to the eval function (could also be generalized).
doubleMoves.sort((a, b) => {
return (color == "w" ? 1 : -1) * (b.eval - a.eval);
});
) {
candidates.push(i);
}
-
const selected = doubleMoves[randInt(candidates.length)].moves;
if (selected.length == 1) return selected[0];
return selected;
return moves;
}
- isAttacked(sq, color, castling) {
- const singleMoveAttack = super.isAttacked(sq, color);
- if (singleMoveAttack) return true;
- if (!!castling) {
- if (this.subTurn == 1)
- // Castling at move 1 could be done into check
- return false;
- return singleMoveAttack;
- }
- // Double-move allowed:
- const curTurn = this.turn;
- this.turn = color;
- const moves1 = super.getAllPotentialMoves();
- this.turn = curTurn;
- for (let move of moves1) {
- this.play(move);
- const res = super.isAttacked(sq, color);
- this.undo(move);
- if (res) return res;
- }
+ isAttacked(sq, color) {
+ // Goal is king capture => no checks
return false;
}
filterValid(moves) {
- if (this.subTurn == 1) {
- return moves.filter(m1 => {
- this.play(m1);
- // NOTE: no recursion because next call will see subTurn == 2
- const res = super.atLeastOneMove();
- this.undo(m1);
- return res;
- });
- }
- return super.filterValid(moves);
+ return moves;
+ }
+
+ getCheckSquares() {
+ return [];
+ }
+
+ getCurrentScore() {
+ const color = this.turn;
+ if (this.kingPos[color][0] < 0) return (color == 'w' ? "0-1" : "1-0");
+ return "*";
}
play(move) {
const firstRank = c == "w" ? V.size.x - 1 : 0;
if (piece == V.KING && move.appear.length > 0) {
- this.kingPos[c][0] = move.appear[0].x;
- this.kingPos[c][1] = move.appear[0].y;
+ this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
this.castleFlags[c] = [V.size.y, V.size.y];
return;
}
const oppCol = V.GetOppCol(c);
+ if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+ // Opponent's king is captured, game over
+ this.kingPos[oppCol] = [-1, -1];
const oppFirstRank = V.size.x - 1 - firstRank;
if (
move.start.x == firstRank && //our rook moves?
) {
const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
this.castleFlags[c][flagIdx] = V.size.y;
- } else if (
+ }
+ else if (
move.end.x == oppFirstRank && //we took opponent rook?
this.castleFlags[oppCol].includes(move.end.y)
) {
this.turn = V.GetOppCol(this.turn);
}
if (this.movesCount > 0) this.subTurn = 3 - this.subTurn;
- super.postUndo(move);
+ this.postUndo(move);
}
- static get VALUES() {
- return {
- p: 1,
- r: 5,
- n: 3,
- b: 3,
- q: 7, //slightly less than in orthodox game
- k: 1000
- };
+ postUndo(move) {
+ if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+ // Opponent's king was captured
+ this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y];
+ super.postUndo(move);
}
- // No alpha-beta here, just adapted min-max at depth 1(+1)
+ // No alpha-beta here, just adapted min-max at depth 2(+1)
getComputerMove() {
+ const maxeval = V.INFINITY;
const color = this.turn;
+ const oppCol = V.GetOppCol(this.turn);
+
+ // Search best (half) move for opponent turn
+ const getBestMoveEval = () => {
+ let score = this.getCurrentScore();
+ if (score != "*") 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();
+ if (score != "*") {
+ // King captured
+ 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;
+ };
+
const moves11 = this.getAllValidMoves();
if (this.movesCount == 0)
// First white move at random:
return moves11[randInt(moves11.length)];
-
let doubleMoves = [];
// Rank moves using a min-max at depth 2
for (let i = 0; i < moves11.length; i++) {
this.play(moves12[j]);
doubleMoves.push({
moves: [moves11[i], moves12[j]],
- eval: this.evalPosition()
+ // Small fluctuations to uniformize play a little
+ eval: getBestMoveEval() + 0.05 - Math.random() / 10
});
this.undo(moves12[j]);
}
}
isAttacked(sq, color, castling) {
- const singleMoveAttack = super.isAttacked(sq, color);
- if (singleMoveAttack) return true;
- if (color == 'b' || !!castling) return singleMoveAttack;
- // Attacks by white: double-move allowed
- const curTurn = this.turn;
- this.turn = 'w';
- const w1Moves = super.getAllPotentialMoves();
- this.turn = curTurn;
- for (let move of w1Moves) {
- this.play(move);
- const res = super.isAttacked(sq, 'w');
- this.undo(move);
- if (res) return res;
- }
+ // Goal is king capture => no checks
return false;
}
+ filterValid(moves) {
+ return moves;
+ }
+
+ getCheckSquares() {
+ return [];
+ }
+
+ getCurrentScore() {
+ const color = this.turn;
+ if (this.kingPos[color][0] < 0) return (color == 'w' ? "0-1" : "1-0");
+ return "*";
+ }
+
play(move) {
move.flags = JSON.stringify(this.aggregateFlags());
if (this.turn == 'b' || this.subTurn == 2)
// Definition of 'c' in base class doesn't work:
const c = move.vanish[0].c;
const piece = move.vanish[0].p;
- if (piece == V.KING) {
- this.kingPos[c][0] = move.appear[0].x;
- this.kingPos[c][1] = move.appear[0].y;
- }
+ if (piece == V.KING)
+ this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
+ if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+ // Opponent's king is captured, game over
+ this.kingPos[move.vanish[1].c] = [-1, -1];
this.updateCastleFlags(move, piece);
}
if (this.subTurn == 2) this.subTurn = 1;
else this.turn = 'b';
this.movesCount--;
- } else {
+ }
+ else {
this.turn = 'w';
this.subTurn = 2;
}
this.postUndo(move);
}
- filterValid(moves) {
- if (this.turn == 'w' && this.subTurn == 1) {
- return moves.filter(m1 => {
- this.play(m1);
- // NOTE: no recursion because next call will see subTurn == 2
- const res = super.atLeastOneMove();
- this.undo(m1);
- return res;
- });
- }
- return super.filterValid(moves);
- }
-
- static get SEARCH_DEPTH() {
- return 1;
+ postUndo(move) {
+ if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+ // Opponent's king was captured
+ this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y];
+ super.postUndo(move);
}
+ // Custom search at depth 1(+1)
getComputerMove() {
- const color = this.turn;
- if (color == 'w') {
+ const getBestWhiteMove = (terminal) => {
// Generate all sequences of 2-moves
- const moves1 = this.getAllValidMoves();
+ let moves1 = this.getAllValidMoves();
moves1.forEach(m1 => {
m1.eval = -V.INFINITY;
m1.move2 = null;
this.play(m1);
- const moves2 = this.getAllValidMoves();
- moves2.forEach(m2 => {
- this.play(m2);
- const eval2 = this.evalPosition();
- this.undo(m2);
- if (eval2 > m1.eval) {
- m1.eval = eval2;
- m1.move2 = m2;
- }
- });
+ if (!!terminal) m1.eval = this.evalPosition();
+ else {
+ const moves2 = this.getAllValidMoves();
+ moves2.forEach(m2 => {
+ this.play(m2);
+ const eval2 = this.evalPosition() + 0.05 - Math.random() / 10;
+ this.undo(m2);
+ if (eval2 > m1.eval) {
+ m1.eval = eval2;
+ m1.move2 = m2;
+ }
+ });
+ }
this.undo(m1);
});
moves1.sort((a, b) => b.eval - a.eval);
+ if (!!terminal)
+ // The move itself doesn't matter, only its eval:
+ return moves1[0];
let candidates = [0];
for (
let i = 1;
const move2 = moves1[idx].move2;
delete moves1[idx]["move2"];
return [moves1[idx], move2];
- }
- // For black at depth 1, super method is fine:
- return super.getComputerMove();
+ };
+
+ const getBestBlackMove = () => {
+ let moves = this.getAllValidMoves();
+ moves.forEach(m => {
+ m.eval = V.INFINITY;
+ this.play(m);
+ const evalM = getBestWhiteMove("terminal").eval
+ this.undo(m);
+ if (evalM < m.eval) m.eval = evalM;
+ });
+ moves.sort((a, b) => a.eval - b.eval);
+ let candidates = [0];
+ for (
+ let i = 1;
+ i < moves.length && moves[i].eval == moves[0].eval;
+ i++
+ ) {
+ candidates.push(i);
+ }
+ const idx = candidates[randInt(candidates.length)];
+ return moves[idx];
+ };
+
+ const color = this.turn;
+ return (color == 'w' ? getBestWhiteMove() : getBestBlackMove());
}
};
(color == 'w' && mvEval > m.eval) ||
(color == 'b' && mvEval < m.eval)
) {
- // TODO: if many second moves have the same eval, only the
- // first is kept. Could be randomized.
m.eval = mvEval;
m.next = m2;
}