Skip to content

Commit

Permalink
Merge pull request #927 from trevorbayless/custom_exceptions
Browse files Browse the repository at this point in the history
Add specific move error exceptions
  • Loading branch information
niklasf committed Oct 15, 2022
2 parents 9a81ad1 + a721c5b commit 1345585
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 42 deletions.
78 changes: 55 additions & 23 deletions chess/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ def result(self) -> str:
return "1/2-1/2" if self.winner is None else ("1-0" if self.winner else "0-1")


class InvalidMoveError(ValueError):
"""Raised when the attempted move is invalid in the current position"""


class IllegalMoveError(ValueError):
"""Raised when the attempted move is illegal in the current position"""


class AmbiguousMoveError(ValueError):
"""Raised when the attempted move is ambiguous in the current position"""


Square = int
SQUARES = [
A1, B1, C1, D1, E1, F1, G1, H1,
Expand Down Expand Up @@ -551,23 +563,29 @@ def from_uci(cls, uci: str) -> Move:
"""
Parses a UCI string.
:raises: :exc:`ValueError` if the UCI string is invalid.
:raises: :exc:`InvalidMoveError` if the UCI string is invalid.
"""
if uci == "0000":
return cls.null()
elif len(uci) == 4 and "@" == uci[1]:
drop = PIECE_SYMBOLS.index(uci[0].lower())
square = SQUARE_NAMES.index(uci[2:])
try:
drop = PIECE_SYMBOLS.index(uci[0].lower())
square = SQUARE_NAMES.index(uci[2:])
except ValueError:
raise InvalidMoveError(f"invalid uci: {uci!r}")
return cls(square, square, drop=drop)
elif 4 <= len(uci) <= 5:
from_square = SQUARE_NAMES.index(uci[0:2])
to_square = SQUARE_NAMES.index(uci[2:4])
promotion = PIECE_SYMBOLS.index(uci[4]) if len(uci) == 5 else None
try:
from_square = SQUARE_NAMES.index(uci[0:2])
to_square = SQUARE_NAMES.index(uci[2:4])
promotion = PIECE_SYMBOLS.index(uci[4]) if len(uci) == 5 else None
except ValueError:
raise InvalidMoveError(f"invalid uci: {uci!r}")
if from_square == to_square:
raise ValueError(f"invalid uci (use 0000 for null moves): {uci!r}")
raise InvalidMoveError(f"invalid uci (use 0000 for null moves): {uci!r}")
return cls(from_square, to_square, promotion=promotion)
else:
raise ValueError(f"expected uci string to be of length 4 or 5: {uci!r}")
raise InvalidMoveError(f"expected uci string to be of length 4 or 5: {uci!r}")

@classmethod
def null(cls) -> Move:
Expand Down Expand Up @@ -2305,14 +2323,14 @@ def find_move(self, from_square: Square, to_square: Square, promotion: Optional[
Castling moves are normalized to king moves by two steps, except in
Chess960.
:raises: :exc:`ValueError` if no matching legal move is found.
:raises: :exc:`IllegalMoveError` if no matching legal move is found.
"""
if promotion is None and self.pawns & BB_SQUARES[from_square] and BB_SQUARES[to_square] & BB_BACKRANKS:
promotion = QUEEN

move = self._from_chess960(self.chess960, from_square, to_square, promotion)
if not self.is_legal(move):
raise ValueError(f"no matching legal move for {move.uci()} ({SQUARE_NAMES[from_square]} -> {SQUARE_NAMES[to_square]}) in {self.fen()}")
raise IllegalMoveError(f"no matching legal move for {move.uci()} ({SQUARE_NAMES[from_square]} -> {SQUARE_NAMES[to_square]}) in {self.fen()}")

return move

Expand Down Expand Up @@ -2938,14 +2956,14 @@ def variation_san(self, variation: Iterable[Move]) -> str:
The board will not be modified as a result of calling this.
:raises: :exc:`ValueError` if any moves in the sequence are illegal.
:raises: :exc:`IllegalMoveError` if any moves in the sequence are illegal.
"""
board = self.copy(stack=False)
san = []

for move in variation:
if not board.is_legal(move):
raise ValueError(f"illegal move {move} in position {board.fen()}")
raise IllegalMoveError(f"illegal move {move} in position {board.fen()}")

if board.turn == WHITE:
san.append(f"{board.fullmove_number}. {board.san_and_push(move)}")
Expand All @@ -2966,7 +2984,11 @@ def parse_san(self, san: str) -> Move:
The returned move is guaranteed to be either legal or a null move.
:raises: :exc:`ValueError` if the SAN is invalid, illegal or ambiguous.
:raises:
:exc:`ValueError` (or specifically an exception specified below) if the SAN is invalid, illegal or ambiguous.
- :exc:`InvalidMoveError` if the SAN is invalid.
- :exc:`IllegalMoveError` if the SAN is illegal.
- :exc:`AmbiguousMoveError` if the SAN is ambiguous.
"""
# Castling.
try:
Expand All @@ -2975,7 +2997,7 @@ def parse_san(self, san: str) -> Move:
elif san in ["O-O-O", "O-O-O+", "O-O-O#", "0-0-0", "0-0-0+", "0-0-0#"]:
return next(move for move in self.generate_castling_moves() if self.is_queenside_castling(move))
except StopIteration:
raise ValueError(f"illegal san: {san!r} in {self.fen()}")
raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}")

# Match normal moves.
match = SAN_REGEX.match(san)
Expand All @@ -2984,9 +3006,9 @@ def parse_san(self, san: str) -> Move:
if san in ["--", "Z0", "0000", "@@@@"]:
return Move.null()
elif "," in san:
raise ValueError(f"unsupported multi-leg move: {san!r}")
raise InvalidMoveError(f"unsupported multi-leg move: {san!r}")
else:
raise ValueError(f"invalid san: {san!r}")
raise InvalidMoveError(f"invalid san: {san!r}")

# Get target square. Mask our own pieces to exclude castling moves.
to_square = SQUARE_NAMES.index(match.group(4))
Expand Down Expand Up @@ -3016,7 +3038,7 @@ def parse_san(self, san: str) -> Move:
if move.promotion == promotion:
return move
else:
raise ValueError(f"missing promotion piece type: {san!r} in {self.fen()}")
raise IllegalMoveError(f"missing promotion piece type: {san!r} in {self.fen()}")
else:
from_mask &= self.pawns

Expand All @@ -3031,12 +3053,12 @@ def parse_san(self, san: str) -> Move:
continue

if matched_move:
raise ValueError(f"ambiguous san: {san!r} in {self.fen()}")
raise AmbiguousMoveError(f"ambiguous san: {san!r} in {self.fen()}")

matched_move = move

if not matched_move:
raise ValueError(f"illegal san: {san!r} in {self.fen()}")
raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}")

return matched_move

Expand All @@ -3047,7 +3069,11 @@ def push_san(self, san: str) -> Move:
Returns the move.
:raises: :exc:`ValueError` if neither legal nor a null move.
:raises:
:exc:`ValueError` (or specifically an exception specified below) if neither legal nor a null move.
- :exc:`InvalidMoveError` if the SAN is invalid.
- :exc:`IllegalMoveError` if the SAN is illegal.
- :exc:`AmbiguousMoveError` if the SAN is ambiguous.
"""
move = self.parse_san(san)
self.push(move)
Expand Down Expand Up @@ -3075,8 +3101,11 @@ def parse_uci(self, uci: str) -> Move:
The returned move is guaranteed to be either legal or a null move.
:raises: :exc:`ValueError` if the move is invalid or illegal in the
:raises:
:exc:`ValueError` (or specifically an exception specified below) if the move is invalid or illegal in the
current position (but not a null move).
- :exc:`InvalidMoveError` if the UCI is invalid.
- :exc:`IllegalMoveError` if the UCI is illegal.
"""
move = Move.from_uci(uci)

Expand All @@ -3087,7 +3116,7 @@ def parse_uci(self, uci: str) -> Move:
move = self._from_chess960(self.chess960, move.from_square, move.to_square, move.promotion, move.drop)

if not self.is_legal(move):
raise ValueError(f"illegal uci: {uci!r} in {self.fen()}")
raise IllegalMoveError(f"illegal uci: {uci!r} in {self.fen()}")

return move

Expand All @@ -3097,8 +3126,11 @@ def push_uci(self, uci: str) -> Move:
Returns the move.
:raises: :exc:`ValueError` if the move is invalid or illegal in the
:raises:
:exc:`ValueError` (or specifically an exception specified below) if the move is invalid or illegal in the
current position (but not a null move).
- :exc:`InvalidMoveError` if the UCI is invalid.
- :exc:`IllegalMoveError` if the UCI is illegal.
"""
move = self.parse_uci(uci)
self.push(move)
Expand Down
2 changes: 1 addition & 1 deletion chess/variant.py
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ def parse_san(self, san: str) -> chess.Move:
uci = "P" + uci
move = chess.Move.from_uci(uci)
if not self.is_legal(move):
raise ValueError(f"illegal drop san: {san!r} in {self.fen()}")
raise chess.IllegalMoveError(f"illegal drop san: {san!r} in {self.fen()}")
return move
else:
return super().parse_san(san)
Expand Down
43 changes: 25 additions & 18 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,16 @@ def test_uci_parsing(self):
self.assertEqual(chess.Move.from_uci("0000").uci(), "0000")

def test_invalid_uci(self):
with self.assertRaises(ValueError):
with self.assertRaises(chess.InvalidMoveError):
chess.Move.from_uci("")

with self.assertRaises(ValueError):
with self.assertRaises(chess.InvalidMoveError):
chess.Move.from_uci("N")

with self.assertRaises(ValueError):
with self.assertRaises(chess.InvalidMoveError):
chess.Move.from_uci("z1g3")

with self.assertRaises(ValueError):
with self.assertRaises(chess.InvalidMoveError):
chess.Move.from_uci("Q@g9")

def test_xboard_move(self):
Expand Down Expand Up @@ -383,9 +383,9 @@ def test_castling(self):
def test_castling_san(self):
board = chess.Board("4k3/8/8/8/8/8/8/4K2R w K - 0 1")
self.assertEqual(board.parse_san("O-O"), chess.Move.from_uci("e1g1"))
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.parse_san("Kg1")
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.parse_san("Kh1")

def test_ninesixty_castling(self):
Expand Down Expand Up @@ -503,9 +503,9 @@ def test_find_move(self):
self.assertEqual(board.find_move(chess.B7, chess.B8, chess.KNIGHT), chess.Move.from_uci("b7b8n"))

# Illegal moves.
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.find_move(chess.D2, chess.D8)
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.find_move(chess.E1, chess.A1)

# Castling.
Expand Down Expand Up @@ -590,6 +590,13 @@ def test_promotion_with_check(self):
board.push_san("d1=Q+")
self.assertEqual(board.fen(), "8/8/8/3R1P2/8/2k2K2/8/r2q4 w - - 0 83")

def test_ambiguous_move(self):
board = chess.Board("8/8/1n6/3R1P2/1n6/2k2K2/3p4/r6r b - - 0 82")
with self.assertRaises(chess.AmbiguousMoveError):
board.parse_san("Rf1")
with self.assertRaises(chess.AmbiguousMoveError):
board.parse_san("Nd5")

def test_scholars_mate(self):
board = chess.Board()

Expand Down Expand Up @@ -723,17 +730,17 @@ def test_lan(self):

def test_san_newline(self):
board = chess.Board("rnbqk2r/ppppppbp/5np1/8/8/5NP1/PPPPPPBP/RNBQK2R w KQkq - 2 4")
with self.assertRaises(ValueError):
with self.assertRaises(chess.InvalidMoveError):
board.parse_san("O-O\n")
with self.assertRaises(ValueError):
with self.assertRaises(chess.InvalidMoveError):
board.parse_san("Nc3\n")

def test_pawn_capture_san_without_file(self):
board = chess.Board("2rq1rk1/pb2bppp/1p2p3/n1ppPn2/2PP4/PP3N2/1B1NQPPP/RB3RK1 b - - 4 13")
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.parse_san("c4")
board = chess.Board("4k3/8/8/4Pp2/8/8/8/4K3 w - f6 0 2")
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.parse_san("f6")

def test_variation_san(self):
Expand Down Expand Up @@ -763,7 +770,7 @@ def test_variation_san(self):

illegal_variation = ['d3h7', 'g8h7', 'f3h6', 'h7g8']
board = chess.Board(fen)
with self.assertRaises(ValueError) as err:
with self.assertRaises(chess.IllegalMoveError) as err:
board.variation_san([chess.Move.from_uci(m) for m in illegal_variation])
message = str(err.exception)
self.assertIn('illegal move', message.lower(),
Expand Down Expand Up @@ -4018,7 +4025,7 @@ def test_parse_san(self):
board.push_san("d5")

# Capture is mandatory.
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.push_san("Nf3")

def test_is_legal(self):
Expand Down Expand Up @@ -4159,15 +4166,15 @@ def test_atomic_castle_with_kings_touching(self):
self.assertEqual(board.fen(), "8/8/8/8/8/8/4k3/2KR3q b - - 1 1")

board = chess.variant.AtomicBoard("8/8/8/8/8/8/5k2/R3K2r w Q - 0 1")
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.push_san("O-O-O")

board = chess.variant.AtomicBoard("8/8/8/8/8/8/6k1/R5Kr w Q - 0 1", chess960=True)
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.push_san("O-O-O")

board = chess.variant.AtomicBoard("8/8/8/8/8/8/4k3/r2RK2r w D - 0 1", chess960=True)
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.push_san("O-O-O")

def test_castling_rights_explode_with_king(self):
Expand Down Expand Up @@ -4495,7 +4502,7 @@ def test_capture_with_promotion(self):

def test_illegal_drop_uci(self):
board = chess.variant.CrazyhouseBoard()
with self.assertRaises(ValueError):
with self.assertRaises(chess.IllegalMoveError):
board.parse_uci("N@f3")

def test_crazyhouse_fen(self):
Expand Down

0 comments on commit 1345585

Please sign in to comment.