Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Handle *some* out-of-spec behaviour explicitly #4563

Closed
wants to merge 11 commits into from
221 changes: 182 additions & 39 deletions src/position.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,7 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th
incremented after Black's move.
*/

unsigned char col, row, token;
size_t idx;
Square sq = SQ_A8;
unsigned char token;
std::istringstream ss(fenStr);

std::memset(this, 0, sizeof(Position));
Expand All @@ -204,87 +202,206 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th

ss >> std::noskipws;

int piece_count = 0;
File file = FILE_A;
Rank rank = RANK_8;

// 1. Piece placement
while ((ss >> token) && !isspace(token))
for (;;)
{
if (isdigit(token))
sq += (token - '0') * EAST; // Advance the given number of files
if (!(ss >> token))
UCI::critical_error("Invalid FEN. Unexpected end of stream.");

if (isspace(token))
break;

if (isdigit(token))
{
const int diff = token - '0';
if (diff < 1 || diff > 8)
UCI::critical_error("Invalid FEN. Invalid number of squares to skip.");
file = File(file + diff);
if (file > FILE_NB)
UCI::critical_error("Invalid FEN. Invalid file reached.");
}
else if (token == '/')
sq += 2 * SOUTH;
{
if (file != FILE_NB)
UCI::critical_error("Invalid FEN. Trying to end rank when not at the end of it.");

else if ((idx = PieceToChar.find(token)) != string::npos) {
--rank;
file = FILE_A;

if (rank < RANK_1)
UCI::critical_error("Invalid FEN. Invalid rank reached.");
}
else
{
const size_t idx = PieceToChar.find(token);
if (idx == string::npos)
UCI::critical_error(std::string("Invalid FEN. Invalid piece: ") + std::string(1, token));
if (++piece_count > 32)
UCI::critical_error("Invalid FEN. More than 32 pieces on the board.");

const Square sq = make_square(file, rank);
put_piece(Piece(idx), sq);
++sq;

++file;
if (file > FILE_NB)
UCI::critical_error("Invalid FEN. Invalid file reached.");
}
}

if (rank != RANK_1 || file != FILE_NB)
UCI::critical_error("Invalid FEN. Board state encoding ended but cursor not at end.");

{
const int pawns_w = count<PAWN>(WHITE);
const int pawns_b = count<PAWN>(BLACK);
if (pawns_w > 8)
UCI::critical_error("Invalid FEN. WHITE has more than 8 pawns.");
if (pawns_b > 8)
UCI::critical_error("Invalid FEN. BLACK has more than 8 pawns.");

const int additional_knights_w = std::max((int)count<KNIGHT>(WHITE) - 2, 0);
const int additional_knights_b = std::max((int)count<KNIGHT>(BLACK) - 2, 0);
const int additional_bishops_w = std::max((int)count<BISHOP>(WHITE) - 2, 0);
const int additional_bishops_b = std::max((int)count<BISHOP>(BLACK) - 2, 0);
const int additional_rooks_w = std::max((int)count<ROOK>(WHITE) - 2, 0);
const int additional_rooks_b = std::max((int)count<ROOK>(BLACK) - 2, 0);
const int additional_queens_w = std::max((int)count<QUEEN>(WHITE) - 1, 0);
const int additional_queens_b = std::max((int)count<QUEEN>(BLACK) - 1, 0);
if (additional_knights_w + additional_bishops_w + additional_rooks_w + additional_queens_w > 8 - pawns_w)
UCI::critical_error("Invalid FEN. Invalid piece configuration for WHITE.");
if (additional_knights_b + additional_bishops_b + additional_rooks_b + additional_queens_b > 8 - pawns_b)
UCI::critical_error("Invalid FEN. Invalid piece configuration for BLACK.");
}

ss >> std::ws;

// 2. Active color
ss >> token;
if (!(ss >> token))
UCI::critical_error("Invalid FEN. Unexpected end of stream.");
if (token != 'w' && token != 'b')
UCI::critical_error(std::string("Invalid FEN. Invalid side to move: ") + std::string(1, token));
sideToMove = (token == 'w' ? WHITE : BLACK);
ss >> token;

ss >> std::ws;

// 3. Castling availability. Compatible with 3 standards: Normal FEN standard,
// Shredder-FEN that uses the letters of the columns on which the rooks began
// the game instead of KQkq and also X-FEN standard that, in case of Chess960,
// if an inner rook is associated with the castling right, the castling tag is
// replaced by the file letter of the involved rook, as for the Shredder-FEN.
while ((ss >> token) && !isspace(token))
int num_castling_rights = 0;
for (;;)
{
if (!(ss >> token))
UCI::critical_error("Invalid FEN. Unexpected end of stream.");

if (isspace(token))
break;

if (num_castling_rights == 0 && token == '-')
break;

if (++num_castling_rights > 4)
UCI::critical_error("Invalid FEN. Maximum of 4 castling rights can be specified.");

Square rsq;
Color c = islower(token) ? BLACK : WHITE;
Piece rook = make_piece(c, ROOK);

token = char(toupper(token));

if (token == 'K')
for (rsq = relative_square(c, SQ_H1); piece_on(rsq) != rook; --rsq) {}

for (rsq = relative_square(c, SQ_H1); piece_on(rsq) != rook && file_of(rsq) >= FILE_A; --rsq) {}
else if (token == 'Q')
for (rsq = relative_square(c, SQ_A1); piece_on(rsq) != rook; ++rsq) {}

for (rsq = relative_square(c, SQ_A1); piece_on(rsq) != rook && file_of(rsq) <= FILE_H; ++rsq) {}
else if (token >= 'A' && token <= 'H')
rsq = make_square(File(token - 'A'), relative_rank(c, RANK_1));

else
continue;
UCI::critical_error(std::string("Invalid FEN. Expected castling rights. Got: ") + std::string(1, token));

if (piece_on(rsq) != rook)
UCI::critical_error("Invalid FEN. Trying to set castling rights without required rook.");

set_castling_right(c, rsq);
}

// 4. En passant square.
// Ignore if square is invalid or not on side to move relative rank 6.
bool enpassant = false;
ss >> std::ws;

if ( ((ss >> col) && (col >= 'a' && col <= 'h'))
&& ((ss >> row) && (row == (sideToMove == WHITE ? '6' : '3'))))
// 4. En passant square. Faux ep-square is ignored. Otherwise invalid ep-square is an error.
bool enpassant = false;
unsigned char col, row;
if (!(ss >> col))
UCI::critical_error("Invalid FEN. Unexpected end of stream.");
if (col != '-')
{
st->epSquare = make_square(File(col - 'a'), Rank(row - '1'));

// En passant square will be considered only if
// a) side to move have a pawn threatening epSquare
// b) there is an enemy pawn in front of epSquare
// c) there is no piece on epSquare or behind epSquare
enpassant = pawn_attacks_bb(~sideToMove, st->epSquare) & pieces(sideToMove, PAWN)
&& (pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove)))
&& !(pieces() & (st->epSquare | (st->epSquare + pawn_push(sideToMove))));
if (!(ss >> row))
UCI::critical_error("Invalid FEN. Unexpected end of stream.");

if ( (col >= 'a' && col <= 'h')
&& (row == (sideToMove == WHITE ? '6' : '3')))
{
st->epSquare = make_square(File(col - 'a'), Rank(row - '1'));

// En passant square will be considered only if
// a) side to move have a pawn threatening epSquare
// b) there is an enemy pawn in front of epSquare
// c) there is no piece on epSquare or behind epSquare
enpassant = pawn_attacks_bb(~sideToMove, st->epSquare) & pieces(sideToMove, PAWN)
&& (pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove)))
&& !(pieces() & (st->epSquare | (st->epSquare + pawn_push(sideToMove))));
}
else
UCI::critical_error("Invalid FEN. Invalid en-passant square.");
}

if (!enpassant)
st->epSquare = SQ_NONE;

// 5-6. Halfmove clock and fullmove number
ss >> std::skipws >> st->rule50 >> gamePly;
ss >> std::skipws;

// 5-6. Halfmove clock and fullmove number. Either none or both must be present.
// If they are not present then values are the same as for startpos.
if (ss >> st->rule50)
{
if (!(ss >> gamePly))
UCI::critical_error("Invalid FEN. Unexpected end of stream or invalid numbers.");
}
else
{
st->rule50 = 0;
gamePly = 1;
}

// Technically, positions with rule50==100 are correct, just no moves can be made further.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite unnecessary IMO, rule50 count doesn't really affect gameplay, and user can easily artifically bypass it anyways.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the 50-move rule and 75-move rule do apply in over-the-board play, they do not apply for certain purposes in endgame theory. In particular, endgame tablebases will allow one to step through a forced with that takes far longer, and this would (presulably) result in a larger count here. Instead of refusing to import the FEN, I think it would be better for Stockfish to clamp rule50 to 150.

// However, due to human stuff, we want to *support* rule50 up to 150.
constexpr int max_rule50_fullmoves = 75;
if (st->rule50 < 0 || st->rule50 > max_rule50_fullmoves * 2)
UCI::critical_error("Invalid FEN. Rule50 counter outside of range, got: " + std::to_string(st->rule50));

// https://chess.stackexchange.com/questions/4113/longest-chess-game-possible-maximum-moves
constexpr int max_moves = max_rule50_fullmoves - 1
+ 32 * max_rule50_fullmoves
+ 6 * 8 * max_rule50_fullmoves
+ 7 * max_rule50_fullmoves
+ 30 * max_rule50_fullmoves;
if (gamePly < 1 || gamePly > max_moves)
UCI::critical_error("Invalid FEN. Full-move counter outside of range, got: " + std::to_string(gamePly));

// Convert from fullmove starting from 1 to gamePly starting from 0,
// handle also common incorrect FEN with fullmove = 0.
gamePly = std::max(2 * (gamePly - 1), 0) + (sideToMove == BLACK);

chess960 = isChess960;
thisThread = th;
set_state();

assert(pos_is_ok());
if (!pos_is_ok())
UCI::critical_error("Invalid FEN.");

set_state();

return *this;
}
Expand Down Expand Up @@ -1292,42 +1409,65 @@ bool Position::pos_is_ok() const {

constexpr bool Fast = true; // Quick (default) or full check?

if ( pieceCount[W_KING] != 1
|| pieceCount[B_KING] != 1)
{
assert(0 && "pos_is_ok: King count");
return false;
}

if ( (sideToMove != WHITE && sideToMove != BLACK)
|| piece_on(square<KING>(WHITE)) != W_KING
|| piece_on(square<KING>(BLACK)) != B_KING
|| ( ep_square() != SQ_NONE
&& relative_rank(sideToMove, ep_square()) != RANK_6))
{
assert(0 && "pos_is_ok: Default");
return false;
}

if (Fast)
return true;

if ( pieceCount[W_KING] != 1
|| pieceCount[B_KING] != 1
|| attackers_to(square<KING>(~sideToMove)) & pieces(sideToMove))
if (attackers_to(square<KING>(~sideToMove)) & pieces(sideToMove))
{
assert(0 && "pos_is_ok: Kings");
return false;
}

if ( (pieces(PAWN) & (Rank1BB | Rank8BB))
|| pieceCount[W_PAWN] > 8
|| pieceCount[B_PAWN] > 8)
{
assert(0 && "pos_is_ok: Pawns");
return false;
}

if ( (pieces(WHITE) & pieces(BLACK))
|| (pieces(WHITE) | pieces(BLACK)) != pieces()
|| popcount(pieces(WHITE)) > 16
|| popcount(pieces(BLACK)) > 16)
{
assert(0 && "pos_is_ok: Bitboards");
return false;
}

for (PieceType p1 = PAWN; p1 <= KING; ++p1)
for (PieceType p2 = PAWN; p2 <= KING; ++p2)
if (p1 != p2 && (pieces(p1) & pieces(p2)))
{
assert(0 && "pos_is_ok: Bitboards");
return false;
}


for (Piece pc : Pieces)
if ( pieceCount[pc] != popcount(pieces(color_of(pc), type_of(pc)))
|| pieceCount[pc] != std::count(board, board + SQUARE_NB, pc))
{
assert(0 && "pos_is_ok: Pieces");
return false;
}

for (Color c : { WHITE, BLACK })
for (CastlingRights cr : {c & KING_SIDE, c & QUEEN_SIDE})
Expand All @@ -1338,7 +1478,10 @@ bool Position::pos_is_ok() const {
if ( piece_on(castlingRookSquare[cr]) != make_piece(c, ROOK)
|| castlingRightsMask[castlingRookSquare[cr]] != cr
|| (castlingRightsMask[square<KING>(c)] & cr) != cr)
{
assert(0 && "pos_is_ok: Castling");
return false;
}
}

return true;
Expand Down
18 changes: 17 additions & 1 deletion src/uci.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,16 @@ namespace {
pos.set(fen, Options["UCI_Chess960"], &states->back(), Threads.main());

// Parse the move list, if any
while (is >> token && (m = UCI::to_move(pos, token)) != MOVE_NONE)
while (is >> token)
{
if (pos.state()->rule50 > 99)
UCI::critical_error("Invalid move. Draw by rule50 reached.");

m = UCI::to_move(pos, token);

if (m == MOVE_NONE)
UCI::critical_error("Invalid moves. Illegal move.");

states->emplace_back();
pos.do_move(m, states->back());
}
Expand Down Expand Up @@ -395,4 +403,12 @@ Move UCI::to_move(const Position& pos, string& str) {
return MOVE_NONE;
}

void UCI::critical_error(const std::string& message) {
// Ideally we would report the error and continue, but UCI does not define such a situation nor
// gives any guarantees as to the program's behaviour for the user to rely on,
// so the safest option is to terminate.
sync_cout << "info string CRITICAL ERROR: " << message << '\n' << sync_endl;
std::terminate();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exiting is an appropriate action here, but I don’t think std::terminate() is the right way to do it. std::terminate() typically results in a core dump, so it should only be used when there is a bug in Stockfish itself. It isn’t the appropriate reaction to invalid input. In this case, I would just call exit() if there are no other threads running, or quick_exit() or _Exit otherwise.

}

} // namespace Stockfish
2 changes: 2 additions & 0 deletions src/uci.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ std::string pv(const Position& pos, Depth depth);
std::string wdl(Value v, int ply);
Move to_move(const Position& pos, std::string& str);

[[noreturn]] void critical_error(const std::string& message);

} // namespace UCI

extern UCI::OptionsMap Options;
Expand Down