Refactor Errors types

Closes #40
This commit is contained in:
Vincent Velociter
2024-08-01 14:48:57 +02:00
parent 8577e982c7
commit 949408b3f8
8 changed files with 192 additions and 134 deletions
+6 -4
View File
@@ -108,7 +108,7 @@ class Board {
/// Parse the board part of a FEN string and returns a Board.
///
/// Throws a [FenError] if the provided FEN string is not valid.
/// Throws a [FenException] if the provided FEN string is not valid.
factory Board.parseFen(String boardFen) {
Board board = Board.empty;
int rank = 7;
@@ -123,18 +123,20 @@ class Board {
if (code < 57) {
file += code - 48;
} else {
if (file >= 8 || rank < 0) throw const FenError('ERR_BOARD');
if (file >= 8 || rank < 0) {
throw const FenException(IllegalFenCause.board);
}
final square = Square(file + rank * 8);
final promoted = i + 1 < boardFen.length && boardFen[i + 1] == '~';
final piece = _charToPiece(c, promoted);
if (piece == null) throw const FenError('ERR_BOARD');
if (piece == null) throw const FenException(IllegalFenCause.board);
if (promoted) i++;
board = board.setPieceAt(square, piece);
file++;
}
}
}
if (rank != 0 || file != 8) throw const FenError('ERR_BOARD');
if (rank != 0 || file != 8) throw const FenException(IllegalFenCause.board);
return board;
}
+100 -2
View File
@@ -630,10 +630,108 @@ class DropMove extends Move {
int get hashCode => Object.hash(to, role);
}
/// An enumeration of the possible causes of an illegal FEN string.
enum IllegalFenCause {
/// The FEN string is not in the correct format.
format,
/// The board part of the FEN string is invalid.
board,
/// The turn part of the FEN string is invalid.
turn,
/// The castling part of the FEN string is invalid.
castling,
/// The en passant part of the FEN string is invalid.
enPassant,
/// The halfmove clock part of the FEN string is invalid.
halfmoveClock,
/// The fullmove number part of the FEN string is invalid.
fullmoveNumber,
/// The remaining checks part of the FEN string is invalid.
remainingChecks,
/// The pockets part of the FEN string is invalid.
pockets,
}
/// An exception thrown when trying to parse an invalid FEN string.
@immutable
class FenError implements Exception {
class FenException implements Exception {
/// Constructs a [FenException] with a [cause].
const FenException(this.cause);
/// The cause of the exception.
final IllegalFenCause cause;
@override
String toString() => 'FenException: ${cause.name}';
}
/// Exception thrown when trying to play an illegal move.
@immutable
class PlayException implements Exception {
/// Constructs a [PlayException] with a [message].
const PlayException(this.message);
/// The exception message.
final String message;
const FenError(this.message);
@override
String toString() => 'PlayException: $message';
}
/// Enumeration of the possible causes of an illegal setup.
enum IllegalSetupCause {
/// There are no pieces on the board.
empty,
/// The player not to move is in check.
oppositeCheck,
/// There are impossibly many checkers, two sliding checkers are
/// aligned, or check is not possible because the last move was a
/// double pawn push.
///
/// Such a position cannot be reached by any sequence of legal moves.
impossibleCheck,
/// There are pawns on the backrank.
pawnsOnBackrank,
/// A king is missing, or there are too many kings.
kings,
/// A variant specific rule is violated.
variant,
}
/// Exception thrown when trying to create a [Position] from an illegal [Setup].
@immutable
class PositionSetupException implements Exception {
/// Constructs a [PositionSetupException] with a [cause].
const PositionSetupException(this.cause);
/// The cause of the exception.
final IllegalSetupCause cause;
static const empty = PositionSetupException(IllegalSetupCause.empty);
static const oppositeCheck =
PositionSetupException(IllegalSetupCause.oppositeCheck);
static const impossibleCheck =
PositionSetupException(IllegalSetupCause.impossibleCheck);
static const pawnsOnBackrank =
PositionSetupException(IllegalSetupCause.pawnsOnBackrank);
static const kings = PositionSetupException(IllegalSetupCause.kings);
static const variant = PositionSetupException(IllegalSetupCause.variant);
@override
String toString() => 'PositionSetupException: ${cause.name}';
}
/// Represents the different possible rules of chess and its variants
+2 -2
View File
@@ -135,11 +135,11 @@ class PgnGame<T extends PgnNodeData> {
///
/// Headers can include an optional 'Variant' and 'Fen' key.
///
/// Throws a [PositionError] if it does not meet basic validity requirements.
/// Throws a [PositionSetupException] if it does not meet basic validity requirements.
static Position startingPosition(PgnHeaders headers,
{bool? ignoreImpossibleCheck}) {
final rule = Rule.fromPgn(headers['Variant']);
if (rule == null) throw PositionError.variant;
if (rule == null) throw PositionSetupException.variant;
if (!headers.containsKey('FEN')) {
return Position.initialPosition(rule);
}
+41 -91
View File
@@ -517,12 +517,12 @@ abstract class Position<T extends Position<T>> {
/// Plays a move and returns the updated [Position].
///
/// Throws a [PlayError] if the move is not legal.
/// Throws a [PlayException] if the move is not legal.
Position<T> play(Move move) {
if (isLegal(move)) {
return playUnchecked(move);
} else {
throw PlayError('Invalid move $move');
throw PlayException('Invalid move $move');
}
}
@@ -622,30 +622,30 @@ abstract class Position<T extends Position<T>> {
/// Returns the SAN of this [Move] and the updated [Position].
///
/// Throws a [PlayError] if the move is not legal.
/// Throws a [PlayException] if the move is not legal.
(Position<T>, String) makeSan(Move move) {
if (isLegal(move)) {
return makeSanUnchecked(move);
} else {
throw PlayError('Invalid move $move');
throw PlayException('Invalid move $move');
}
}
/// Returns the SAN of this [Move] from the current [Position].
///
/// Throws a [PlayError] if the move is not legal.
/// Throws a [PlayException] if the move is not legal.
@Deprecated('Use makeSan instead')
String toSan(Move move) {
if (isLegal(move)) {
return makeSanUnchecked(move).$2;
} else {
throw PlayError('Invalid move $move');
throw PlayException('Invalid move $move');
}
}
/// Returns the SAN representation of the [Move] with the updated [Position].
///
/// Throws a [PlayError] if the move is not legal.
/// Throws a [PlayException] if the move is not legal.
@Deprecated('Use makeSan instead')
(Position<T>, String) playToSan(Move move) {
if (isLegal(move)) {
@@ -658,7 +658,7 @@ abstract class Position<T extends Position<T>> {
: san;
return (newPos, suffixed);
} else {
throw PlayError('Invalid move $move');
throw PlayException('Invalid move $move');
}
}
@@ -675,27 +675,27 @@ abstract class Position<T extends Position<T>> {
/// Checks the legality of this position.
///
/// Throws a [PositionError] if it does not meet basic validity requirements.
/// Throws a [PositionSetupException] if it does not meet basic validity requirements.
void validate({bool? ignoreImpossibleCheck}) {
if (board.occupied.isEmpty) {
throw PositionError.empty;
throw PositionSetupException.empty;
}
if (board.kings.size != 2) {
throw PositionError.kings;
throw PositionSetupException.kings;
}
final ourKing = board.kingOf(turn);
if (ourKing == null) {
throw PositionError.kings;
throw PositionSetupException.kings;
}
final otherKing = board.kingOf(turn.opposite);
if (otherKing == null) {
throw PositionError.kings;
throw PositionSetupException.kings;
}
if (kingAttackers(otherKing, turn).isNotEmpty) {
throw PositionError.oppositeCheck;
throw PositionSetupException.oppositeCheck;
}
if (SquareSet.backranks.isIntersected(board.pawns)) {
throw PositionError.pawnsOnBackrank;
throw PositionSetupException.pawnsOnBackrank;
}
final skipImpossibleCheck = ignoreImpossibleCheck ?? false;
if (!skipImpossibleCheck) {
@@ -734,7 +734,7 @@ abstract class Position<T extends Position<T>> {
/// Checks if checkers are legal in this position.
///
/// Throws a [PositionError.impossibleCheck] if it does not meet validity
/// Throws a [PositionSetupException.impossibleCheck] if it does not meet validity
/// requirements.
void _validateCheckers(Square ourKing) {
final checkers = kingAttackers(ourKing, turn.opposite);
@@ -752,14 +752,14 @@ abstract class Position<T extends Position<T>> {
.withoutSquare(pushedTo)
.withSquare(pushedFrom))
.isNotEmpty)) {
throw PositionError.impossibleCheck;
throw PositionSetupException.impossibleCheck;
}
} else {
// Multiple sliding checkers aligned with king.
if (checkers.size > 2 ||
(checkers.size == 2 &&
ray(checkers.first!, checkers.last!).has(ourKing))) {
throw PositionError.impossibleCheck;
throw PositionSetupException.impossibleCheck;
}
}
}
@@ -1048,7 +1048,7 @@ class Chess extends Position<Chess> {
/// Set up a playable [Chess] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// Throws a [PositionSetupException] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
@@ -1119,7 +1119,7 @@ class Antichess extends Position<Antichess> {
/// Set up a playable [Antichess] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// Throws a [PositionSetupException] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
@@ -1133,10 +1133,10 @@ class Antichess extends Position<Antichess> {
@override
void validate({bool? ignoreImpossibleCheck}) {
if (board.occupied.isEmpty) {
throw PositionError.empty;
throw PositionSetupException.empty;
}
if (SquareSet.backranks.isIntersected(board.pawns)) {
throw PositionError.pawnsOnBackrank;
throw PositionSetupException.pawnsOnBackrank;
}
}
@@ -1257,7 +1257,7 @@ class Atomic extends Position<Atomic> {
/// Set up a playable [Atomic] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// Throws a [PositionSetupException] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
@@ -1284,24 +1284,24 @@ class Atomic extends Position<Atomic> {
/// Checks the legality of this position.
///
/// Validation is like chess, but it allows our king to be missing.
/// Throws a [PositionError] if it does not meet basic validity requirements.
/// Throws a [PositionSetupException] if it does not meet basic validity requirements.
@override
void validate({bool? ignoreImpossibleCheck}) {
if (board.occupied.isEmpty) {
throw PositionError.empty;
throw PositionSetupException.empty;
}
if (board.kings.size > 2) {
throw PositionError.kings;
throw PositionSetupException.kings;
}
final otherKing = board.kingOf(turn.opposite);
if (otherKing == null) {
throw PositionError.kings;
throw PositionSetupException.kings;
}
if (kingAttackers(otherKing, turn).isNotEmpty) {
throw PositionError.oppositeCheck;
throw PositionSetupException.oppositeCheck;
}
if (SquareSet.backranks.isIntersected(board.pawns)) {
throw PositionError.pawnsOnBackrank;
throw PositionSetupException.pawnsOnBackrank;
}
final skipImpossibleCheck = ignoreImpossibleCheck ?? false;
final ourKing = board.kingOf(turn);
@@ -1474,7 +1474,7 @@ class Crazyhouse extends Position<Crazyhouse> {
/// Set up a playable [Crazyhouse] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// Throws a [PositionSetupException] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
@@ -1494,13 +1494,13 @@ class Crazyhouse extends Position<Crazyhouse> {
void validate({bool? ignoreImpossibleCheck}) {
super.validate(ignoreImpossibleCheck: ignoreImpossibleCheck);
if (pockets == null) {
throw PositionError.variant;
throw PositionSetupException.variant;
} else {
if (pockets!.count(Role.king) > 0) {
throw PositionError.kings;
throw PositionSetupException.kings;
}
if (pockets!.size + board.occupied.size > 64) {
throw PositionError.variant;
throw PositionSetupException.variant;
}
}
}
@@ -1601,7 +1601,7 @@ class KingOfTheHill extends Position<KingOfTheHill> {
/// Set up a playable [KingOfTheHill] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// Throws a [PositionSetupException] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
@@ -1682,13 +1682,13 @@ class ThreeCheck extends Position<ThreeCheck> {
/// Set up a playable [ThreeCheck] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// Throws a [PositionSetupException] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
factory ThreeCheck.fromSetup(Setup setup, {bool? ignoreImpossibleCheck}) {
if (setup.remainingChecks == null) {
throw PositionError.variant;
throw PositionSetupException.variant;
} else {
final pos = ThreeCheck(
board: setup.board,
@@ -1850,7 +1850,7 @@ class RacingKings extends Position<RacingKings> {
/// Set up a playable [RacingKings] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// Throws a [PositionSetupException] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
@@ -1937,21 +1937,21 @@ class Horde extends Position<Horde> {
@override
void validate({bool? ignoreImpossibleCheck}) {
if (board.occupied.isEmpty) {
throw PositionError.empty;
throw PositionSetupException.empty;
}
if (board.kings.size != 1) {
throw PositionError.kings;
throw PositionSetupException.kings;
}
final otherKing = board.kingOf(turn.opposite);
if (otherKing != null && kingAttackers(otherKing, turn).isNotEmpty) {
throw PositionError.oppositeCheck;
throw PositionSetupException.oppositeCheck;
}
// white can have pawns on back rank
if (SquareSet.backranks.isIntersected(board.black.intersect(board.pawns))) {
throw PositionError.pawnsOnBackrank;
throw PositionSetupException.pawnsOnBackrank;
}
final skipImpossibleCheck = ignoreImpossibleCheck ?? false;
@@ -2273,56 +2273,6 @@ class Outcome {
}
}
enum IllegalSetup {
/// There are no pieces on the board.
empty,
/// The player not to move is in check.
oppositeCheck,
/// There are impossibly many checkers, two sliding checkers are
/// aligned, or check is not possible because the last move was a
/// double pawn push.
///
/// Such a position cannot be reached by any sequence of legal moves.
impossibleCheck,
/// There are pawns on the backrank.
pawnsOnBackrank,
/// A king is missing, or there are too many kings.
kings,
/// A variant specific rule is violated.
variant,
}
@immutable
class PlayError implements Exception {
final String message;
const PlayError(this.message);
@override
String toString() => 'PlayError($message)';
}
/// Error when trying to create a [Position] from an illegal [Setup].
@immutable
class PositionError implements Exception {
final IllegalSetup cause;
const PositionError(this.cause);
static const empty = PositionError(IllegalSetup.empty);
static const oppositeCheck = PositionError(IllegalSetup.oppositeCheck);
static const impossibleCheck = PositionError(IllegalSetup.impossibleCheck);
static const pawnsOnBackrank = PositionError(IllegalSetup.pawnsOnBackrank);
static const kings = PositionError(IllegalSetup.kings);
static const variant = PositionError(IllegalSetup.variant);
@override
String toString() => 'PositionError(${cause.name})';
}
@immutable
class Castles {
/// SquareSet of rooks that have not moved yet.
+18 -16
View File
@@ -63,10 +63,10 @@ class Setup {
/// * Accepts multiple spaces and underscores (`_`) as separators between
/// FEN fields.
///
/// Throws a [FenError] if the provided FEN is not valid.
/// Throws a [FenException] if the provided FEN is not valid.
factory Setup.parseFen(String fen) {
final parts = fen.split(RegExp(r'[\s_]+'));
if (parts.isEmpty) throw const FenError('ERR_FEN');
if (parts.isEmpty) throw const FenException(IllegalFenCause.format);
// board and pockets
final boardPart = parts.removeAt(0);
@@ -75,7 +75,7 @@ class Setup {
if (boardPart.endsWith(']')) {
final pocketStart = boardPart.indexOf('[');
if (pocketStart == -1) {
throw const FenError('ERR_FEN');
throw const FenException(IllegalFenCause.format);
}
board = Board.parseFen(boardPart.substring(0, pocketStart));
pockets = _parsePockets(
@@ -101,7 +101,7 @@ class Setup {
} else if (turnPart == 'b') {
turn = Side.black;
} else {
throw const FenError('ERR_TURN');
throw const FenException(IllegalFenCause.turn);
}
}
@@ -120,7 +120,9 @@ class Setup {
final epPart = parts.removeAt(0);
if (epPart != '-') {
epSquare = Square.parse(epPart);
if (epSquare == null) throw const FenError('ERR_EP_SQUARE');
if (epSquare == null) {
throw const FenException(IllegalFenCause.enPassant);
}
}
}
@@ -133,21 +135,21 @@ class Setup {
}
final halfmoves = halfmovePart != null ? _parseSmallUint(halfmovePart) : 0;
if (halfmoves == null) {
throw const FenError('ERR_HALFMOVES');
throw const FenException(IllegalFenCause.halfmoveClock);
}
final fullmovesPart = parts.isNotEmpty ? parts.removeAt(0) : null;
final fullmoves =
fullmovesPart != null ? _parseSmallUint(fullmovesPart) : 1;
if (fullmoves == null) {
throw const FenError('ERR_FULLMOVES');
throw const FenException(IllegalFenCause.fullmoveNumber);
}
final remainingChecksPart = parts.isNotEmpty ? parts.removeAt(0) : null;
(int, int)? remainingChecks;
if (remainingChecksPart != null) {
if (earlyRemainingChecks != null) {
throw const FenError('ERR_REMAINING_CHECKS');
throw const FenException(IllegalFenCause.remainingChecks);
}
remainingChecks = _parseRemainingChecks(remainingChecksPart);
} else if (earlyRemainingChecks != null) {
@@ -155,7 +157,7 @@ class Setup {
}
if (parts.isNotEmpty) {
throw const FenError('ERR_FEN');
throw const FenException(IllegalFenCause.format);
}
return Setup(
@@ -269,14 +271,14 @@ class Pockets {
Pockets _parsePockets(String pocketPart) {
if (pocketPart.length > 64) {
throw const FenError('ERR_POCKETS');
throw const FenException(IllegalFenCause.pockets);
}
Pockets pockets = Pockets.empty;
for (int i = 0; i < pocketPart.length; i++) {
final c = pocketPart[i];
final piece = Piece.fromChar(c);
if (piece == null) {
throw const FenError('ERR_POCKETS');
throw const FenException(IllegalFenCause.pockets);
}
pockets = pockets.increment(piece.color, piece.role);
}
@@ -289,18 +291,18 @@ Pockets _parsePockets(String pocketPart) {
final white = _parseSmallUint(parts[1]);
final black = _parseSmallUint(parts[2]);
if (white == null || white > 3 || black == null || black > 3) {
throw const FenError('ERR_REMAINING_CHECKS');
throw const FenException(IllegalFenCause.remainingChecks);
}
return (3 - white, 3 - black);
} else if (parts.length == 2) {
final white = _parseSmallUint(parts[0]);
final black = _parseSmallUint(parts[1]);
if (white == null || white > 3 || black == null || black > 3) {
throw const FenError('ERR_REMAINING_CHECKS');
throw const FenException(IllegalFenCause.remainingChecks);
}
return (white, black);
} else {
throw const FenError('ERR_REMAINING_CHECKS');
throw const FenException(IllegalFenCause.remainingChecks);
}
}
@@ -327,7 +329,7 @@ SquareSet _parseCastlingFen(Board board, String castlingPart) {
backrank)
.squares;
} else {
throw const FenError('ERR_CASTLING');
throw const FenException(IllegalFenCause.castling);
}
for (final square in candidates) {
if (board.kings.has(square)) break;
@@ -339,7 +341,7 @@ SquareSet _parseCastlingFen(Board board, String castlingPart) {
}
if ((const SquareSet.fromRank(Rank.first) & unmovedRooks).size > 2 ||
(const SquareSet.fromRank(Rank.eighth) & unmovedRooks).size > 2) {
throw const FenError('ERR_CASTLING');
throw const FenException(IllegalFenCause.castling);
}
return unmovedRooks;
}