if (!V.IsGoodPosition(fenParsed.position))
return false;
// 2) Check turn
- if (!fenParsed.turn || !["w","b"].includes(fenParsed.turn))
+ if (!fenParsed.turn || !V.IsGoodTurn(fenParsed.turn))
return false;
- // 3) Check flags
+ // 3) Check moves count
+ if (!fenParsed.movesCount || !(parseInt(fenParsed.movesCount) >= 0))
+ return false;
+ // 4) Check flags
if (V.HasFlags && (!fenParsed.flags || !V.IsGoodFlags(fenParsed.flags)))
return false;
- // 4) Check enpassant
- if (V.HasEnpassant)
+ // 5) Check enpassant
+ if (V.HasEnpassant &&
+ (!fenParsed.enpassant || !V.IsGoodEnpassant(fenParsed.enpassant)))
{
- if (!fenParsed.enpassant)
- return false;
- if (fenParsed.enpassant != "-")
- {
- const ep = V.SquareToCoords(fenParsed.enpassant);
- if (ep.y < 0 || ep.y > V.size.y || isNaN(ep.x) || ep.x < 0 || ep.x > V.size.x)
- return false;
- }
+ return false;
}
return true;
}
return true;
}
+ // For FEN checking
+ static IsGoodTurn(turn)
+ {
+ return ["w","b"].includes(turn);
+ }
+
// For FEN checking
static IsGoodFlags(flags)
{
return !!flags.match(/^[01]{4,4}$/);
}
- // 3 --> d (column letter from number)
- static GetColumn(colnum)
+ static IsGoodEnpassant(enpassant)
+ {
+ if (enpassant != "-")
+ {
+ const ep = V.SquareToCoords(fenParsed.enpassant);
+ if (isNaN(ep.x) || !V.OnBoard(ep))
+ return false;
+ }
+ return true;
+ }
+
+ // 3 --> d (column number to letter)
+ static CoordToColumn(colnum)
{
return String.fromCharCode(97 + colnum);
}
+ // d --> 3 (column letter to number)
+ static ColumnToCoord(column)
+ {
+ return column.charCodeAt(0) - 97;
+ }
+
// a4 --> {x:3,y:0}
static SquareToCoords(sq)
{
return {
+ // NOTE: column is always one char => max 26 columns
+ // row is counted from black side => subtraction
x: V.size.x - parseInt(sq.substr(1)),
y: sq[0].charCodeAt() - 97
};
// {x:0,y:4} --> e8
static CoordsToSquare(coords)
{
- return V.GetColumn(coords.y) + (V.size.x - coords.x);
+ return V.CoordToColumn(coords.y) + (V.size.x - coords.x);
}
// Aggregates flags into one object
// 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)
+ // TODO: next conditions are first for Atomic, and third for Checkered
+ if (move.appear.length > 0 && move.appear[0].p == V.PAWN && ["w","b"].includes(move.appear[0].c) && Math.abs(sx - ex) == 2)
{
return {
x: (sx + ex)/2,
return (this.turn == side && this.getColor(x,y) == side);
}
- // On which squares is opponent under check after our move ? (for interface)
- getCheckSquares(move)
+ // On which squares is color under check ? (for interface)
+ getCheckSquares(color)
{
- this.play(move);
- const color = this.turn; //opponent
- let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)])
+ return this.isAttacked(this.kingPos[color], [this.getOppCol(color)])
? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate!
: [];
- this.undo(move);
- return res;
}
/////////////
// Get random squares for bishops
let randIndex = 2 * _.random(3);
- let bishop1Pos = positions[randIndex];
+ const bishop1Pos = positions[randIndex];
// The second bishop must be on a square of different color
let randIndex_tmp = 2 * _.random(3) + 1;
- let bishop2Pos = positions[randIndex_tmp];
+ const 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 = _.random(5);
- let knight1Pos = positions[randIndex];
+ const knight1Pos = positions[randIndex];
positions.splice(randIndex, 1);
randIndex = _.random(4);
- let knight2Pos = positions[randIndex];
+ const knight2Pos = positions[randIndex];
positions.splice(randIndex, 1);
// Get random square for queen
randIndex = _.random(3);
- let queenPos = positions[randIndex];
+ const queenPos = 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];
+ // Rooks and king positions are now fixed,
+ // because of the ordering rook-king-rook
+ const rook1Pos = positions[0];
+ const kingPos = positions[1];
+ const rook2Pos = positions[2];
// Finally put the shuffled pieces in the board array
pieces[c][rook1Pos] = 'r';
{
position: fenParts[0],
turn: fenParts[1],
+ movesCount: fenParts[2],
};
- let nextIdx = 2;
+ let nextIdx = 3;
if (V.HasFlags)
Object.assign(res, {flags: fenParts[nextIdx++]});
if (V.HasEnpassant)
// Return current fen (game state)
getFen()
{
- return this.getBaseFen() + " " + this.turn +
+ return this.getBaseFen() + " " +
+ this.getTurnFen() + " " + this.movesCount +
(V.HasFlags ? (" " + this.getFlagsFen()) : "") +
(V.HasEnpassant ? (" " + this.getEnpassantFen()) : "");
}
return position;
}
+ getTurnFen()
+ {
+ return this.turn;
+ }
+
// Flags part of the FEN string
getFlagsFen()
{
// INITIALIZATION
// Fen string fully describes the game state
- constructor(fen, moves)
+ constructor(fen)
{
- this.moves = moves;
const fenParsed = V.ParseFen(fen);
this.board = V.GetBoard(fenParsed.position);
- this.turn = (fenParsed.turn || "w");
+ this.turn = fenParsed.turn[0]; //[0] to work with MarseilleRules
+ this.movesCount = parseInt(fenParsed.movesCount);
this.setOtherVariables(fen);
}
return (color=="w" ? "b" : "w");
}
- get lastMove()
- {
- const L = this.moves.length;
- return (L>0 ? this.moves[L-1] : null);
- }
-
// Pieces codes (for a clearer code)
static get PAWN() { return 'p'; }
static get ROOK() { return 'r'; }
}
}
- // Build a regular move from its initial and destination squares; tr: transformation
+ // Build a regular move from its initial and destination squares.
+ // tr: transformation
getBasicMove([sx,sy], [ex,ey], tr)
{
let mv = new Move({
return mv;
}
- // Generic method to find possible moves of non-pawn pieces ("sliding or jumping")
+ // Generic method to find possible moves of non-pawn pieces:
+ // "sliding or jumping"
getSlideNJumpMoves([x,y], steps, oneStep)
{
const color = this.getColor(x,y);
const lastRank = (color == "w" ? 0 : sizeX-1);
const pawnColor = this.getColor(x,y); //can be different for checkered
- if (x+shiftX >= 0 && x+shiftX < sizeX) //TODO: always true
+ // NOTE: next condition is generally true (no pawn on last rank)
+ if (x+shiftX >= 0 && x+shiftX < sizeX)
{
const finalPieces = x + shiftX == lastRank
? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]
// What are the queen moves from square x,y ?
getPotentialQueenMoves(sq)
{
- return this.getSlideNJumpMoves(sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
+ return this.getSlideNJumpMoves(sq,
+ V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
}
// What are the king moves from square x,y ?
continue;
// If this code is reached, rooks and king are on initial position
- // Nothing on the path of the king (and no checks; OK also if y==finalSquare)?
+ // Nothing on the path of the king ?
+ // (And no checks; OK also if y==finalSquare)
let step = finalSquares[castleSide][0] < y ? -1 : 1;
for (i=y; i!=finalSquares[castleSide][0]; i+=step)
{
if (this.isAttacked([x,i], [oppCol]) || (this.board[x][i] != V.EMPTY &&
// NOTE: next check is enough, because of chessboard constraints
- (this.getColor(x,i) != c || ![V.KING,V.ROOK].includes(this.getPiece(x,i)))))
+ (this.getColor(x,i) != c
+ || ![V.KING,V.ROOK].includes(this.getPiece(x,i)))))
{
continue castlingCheck;
}
////////////////////
// MOVES VALIDATION
+ // For the interface: possible moves for the current turn from square sq
getPossibleMovesFrom(sq)
{
- // Assuming color is right (already checked)
return this.filterValid( this.getPotentialMovesFrom(sq) );
}
{
if (moves.length == 0)
return [];
- return moves.filter(m => { return !this.underCheck(m); });
+ const color = this.turn;
+ return moves.filter(m => {
+ this.play(m);
+ const res = !this.underCheck(color);
+ this.undo(m);
+ return res;
+ });
}
- // Search for all valid moves considering current turn (for engine and game end)
+ // Search for all valid moves considering current turn
+ // (for engine and game end)
getAllValidMoves()
{
const color = this.turn;
{
for (let j=0; j<V.size.y; j++)
{
- // Next condition "!= oppCol" = harmless hack to work with checkered variant
+ // Next condition "!= oppCol" to work with checkered variant
if (this.board[i][j] != V.EMPTY && this.getColor(i,j) != oppCol)
- Array.prototype.push.apply(potentialMoves, this.getPotentialMovesFrom([i,j]));
+ {
+ Array.prototype.push.apply(potentialMoves,
+ this.getPotentialMovesFrom([i,j]));
+ }
}
}
- // NOTE: prefer lazy undercheck tests, letting the king being taken?
- // No: if happen on last 1/2 move, could lead to forbidden moves, wrong evals
return this.filterValid(potentialMoves);
}
return false;
}
- // Check if pieces of color in array 'colors' are attacking (king) on square x,y
+ // Check if pieces of color in 'colors' are attacking (king) on square x,y
isAttacked(sq, colors)
{
return (this.isAttackedByPawn(sq, colors)
return false;
}
- // Is current player under check after his move ?
- underCheck(move)
+ // Is color under check after his move ?
+ underCheck(color)
{
- const color = this.turn;
- this.play(move);
- let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]);
- this.undo(move);
- return res;
+ return this.isAttacked(this.kingPos[color], [this.getOppCol(color)]);
}
/////////////////
board[psq.x][psq.y] = psq.c + psq.p;
}
- // Before move is played, update variables + flags
+ // After move is played, update variables + flags
updateVariables(move)
{
- const piece = this.getPiece(move.start.x,move.start.y);
- const c = this.turn;
+ let piece = undefined;
+ let c = undefined;
+ if (move.vanish.length >= 1)
+ {
+ // Usual case, something is moved
+ piece = move.vanish[0].p;
+ c = move.vanish[0].c;
+ }
+ else
+ {
+ // Crazyhouse-like variants
+ piece = move.appear[0].p;
+ c = move.appear[0].c;
+ }
+ if (c == "c") //if (!["w","b"].includes(c))
+ {
+ // 'c = move.vanish[0].c' doesn't work for Checkered
+ c = this.getOppCol(this.turn);
+ }
const firstRank = (c == "w" ? V.size.x-1 : 0);
// Update king position + flags
{
this.kingPos[c][0] = move.appear[0].x;
this.kingPos[c][1] = move.appear[0].y;
- this.castleFlags[c] = [false,false];
+ if (V.HasFlags)
+ this.castleFlags[c] = [false,false];
return;
}
- const oppCol = this.getOppCol(c);
- const oppFirstRank = (V.size.x-1) - firstRank;
- if (move.start.x == firstRank //our rook moves?
- && this.INIT_COL_ROOK[c].includes(move.start.y))
- {
- const flagIdx = (move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1);
- this.castleFlags[c][flagIdx] = false;
- }
- else if (move.end.x == oppFirstRank //we took opponent rook?
- && this.INIT_COL_ROOK[oppCol].includes(move.end.y))
+ if (V.HasFlags)
{
- const flagIdx = (move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1);
- this.castleFlags[oppCol][flagIdx] = false;
+ // Update castling flags if rooks are moved
+ const oppCol = this.getOppCol(c);
+ const oppFirstRank = (V.size.x-1) - firstRank;
+ if (move.start.x == firstRank //our rook moves?
+ && this.INIT_COL_ROOK[c].includes(move.start.y))
+ {
+ const flagIdx = (move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1);
+ this.castleFlags[c][flagIdx] = false;
+ }
+ else if (move.end.x == oppFirstRank //we took opponent rook?
+ && this.INIT_COL_ROOK[oppCol].includes(move.end.y))
+ {
+ const flagIdx = (move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1);
+ this.castleFlags[oppCol][flagIdx] = false;
+ }
}
}
// if (!ingame) this.states.push(this.getFen());
if (!!ingame)
- move.notation = [this.getNotation(move), this.getLongNotation(move)];
+ move.notation = this.getNotation(move);
if (V.HasFlags)
move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
- this.updateVariables(move);
- this.moves.push(move);
if (V.HasEnpassant)
this.epSquares.push( this.getEpSquare(move) );
- this.turn = this.getOppCol(this.turn);
V.PlayOnBoard(this.board, move);
+ this.turn = this.getOppCol(this.turn);
+ this.movesCount++;
+ this.updateVariables(move);
if (!!ingame)
{
undo(move)
{
- V.UndoOnBoard(this.board, move);
- this.turn = this.getOppCol(this.turn);
if (V.HasEnpassant)
this.epSquares.pop();
- this.moves.pop();
- this.unupdateVariables(move);
if (V.HasFlags)
this.disaggregateFlags(JSON.parse(move.flags));
+ V.UndoOnBoard(this.board, move);
+ this.turn = this.getOppCol(this.turn);
+ this.movesCount--;
+ this.unupdateVariables(move);
// DEBUG:
// if (this.getFen() != this.states[this.states.length-1])
///////////////
// END OF GAME
- // Check for 3 repetitions (position + flags + turn)
- checkRepetition()
- {
- if (!this.hashStates)
- this.hashStates = {};
- const startIndex =
- Object.values(this.hashStates).reduce((a,b) => { return a+b; }, 0)
- // Update this.hashStates with last move (or all moves if continuation)
- // NOTE: redundant storage, but faster and moderate size
- for (let i=startIndex; i<this.moves.length; i++)
- {
- const move = this.moves[i];
- if (!this.hashStates[move.hash])
- this.hashStates[move.hash] = 1;
- else
- this.hashStates[move.hash]++;
- }
- return Object.values(this.hashStates).some(elt => { return (elt >= 3); });
- }
-
// Is game over ? And if yes, what is the score ?
checkGameOver()
{
- if (this.checkRepetition())
- return "1/2";
-
if (this.atLeastOneMove()) // game not over
return "*";
if (!this.isAttacked(this.kingPos[color], [this.getOppCol(color)]))
return "1/2";
// OK, checkmate
- return color == "w" ? "0-1" : "1-0";
+ return (color == "w" ? "0-1" : "1-0");
}
///////////////
// Rank moves using a min-max at depth 2
for (let i=0; i<moves1.length; i++)
{
- moves1[i].eval = (color=="w" ? -1 : 1) * maxeval; //very low, I'm checkmated
+ // Initial self evaluation is very low: "I'm checkmated"
+ moves1[i].eval = (color=="w" ? -1 : 1) * maxeval;
this.play(moves1[i]);
let eval2 = undefined;
if (this.atLeastOneMove())
{
- eval2 = (color=="w" ? 1 : -1) * maxeval; //initialized with checkmate value
+ // Initial enemy evaluation is very low too, for him
+ eval2 = (color=="w" ? 1 : -1) * maxeval;
// Second half-move:
let moves2 = this.getAllValidMoves("computer");
for (let j=0; j<moves2.length; j++)
const score = this.checkGameEnd();
evalPos = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
}
- if ((color == "w" && evalPos < eval2) || (color=="b" && evalPos > eval2))
+ if ((color == "w" && evalPos < eval2)
+ || (color=="b" && evalPos > eval2))
+ {
eval2 = evalPos;
+ }
this.undo(moves2[j]);
}
}
this.alphabeta(V.SEARCH_DEPTH-1, -maxeval, maxeval);
this.undo(moves1[i]);
}
- moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
+ moves1.sort( (a,b) => {
+ return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
}
else
return currentBest;
- //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
+// console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
candidates = [0];
for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
/////////////////////////
// Context: just before move is played, turn hasn't changed
+ // TODO: un-ambiguous notation (switch on piece type, check directions...)
getNotation(move)
{
if (move.appear.length == 2 && move.appear[0].p == V.KING) //castle
if (move.vanish.length > move.appear.length)
{
// Capture
- const startColumn = String.fromCharCode(97 + move.start.y);
+ const startColumn = V.CoordToColumn(move.start.y);
notation = startColumn + "x" + finalSquare;
}
else //no capture
notation = finalSquare;
- if (move.appear.length > 0 && piece != move.appear[0].p) //promotion
+ if (move.appear.length > 0 && move.appear[0].p != V.PAWN) //promotion
notation += "=" + move.appear[0].p.toUpperCase();
return notation;
}
}
// The score is already computed when calling this function
- getPGN(mycolor, score, fenStart, mode)
+ getPGN(moves, mycolor, score, fenStart, mode)
{
let pgn = "";
- pgn += '[Site "vchess.club"]<br>';
+ pgn += '[Site "vchess.club"]\n';
const opponent = mode=="human" ? "Anonymous" : "Computer";
- pgn += '[Variant "' + variant + '"]<br>';
- pgn += '[Date "' + getDate(new Date()) + '"]<br>';
- pgn += '[White "' + (mycolor=='w'?'Myself':opponent) + '"]<br>';
- pgn += '[Black "' + (mycolor=='b'?'Myself':opponent) + '"]<br>';
- pgn += '[FenStart "' + fenStart + '"]<br>';
- pgn += '[Fen "' + this.getFen() + '"]<br>';
- pgn += '[Result "' + score + '"]<br><br>';
-
- // Standard PGN
- for (let i=0; i<this.moves.length; i++)
- {
- if (i % 2 == 0)
- pgn += ((i/2)+1) + ".";
- pgn += this.moves[i].notation[0] + " ";
- }
- pgn += "<br><br>";
-
- // "Complete moves" PGN (helping in ambiguous cases)
- for (let i=0; i<this.moves.length; i++)
+ pgn += '[Variant "' + variant + '"]\n';
+ pgn += '[Date "' + getDate(new Date()) + '"]\n';
+ // TODO: later when users are a bit less anonymous, use better names
+ const whiteName = ["human","computer"].includes(mode)
+ ? (mycolor=='w'?'Myself':opponent)
+ : "analyze";
+ const blackName = ["human","computer"].includes(mode)
+ ? (mycolor=='b'?'Myself':opponent)
+ : "analyze";
+ pgn += '[White "' + whiteName + '"]\n';
+ pgn += '[Black "' + blackName + '"]\n';
+ pgn += '[Fen "' + fenStart + '"]\n';
+ pgn += '[Result "' + score + '"]\n\n';
+
+ // Print moves
+ for (let i=0; i<moves.length; i++)
{
if (i % 2 == 0)
pgn += ((i/2)+1) + ".";
- pgn += this.moves[i].notation[1] + " ";
+ pgn += moves[i].notation + " ";
}
- return pgn;
+ return pgn + "\n";
}
}