Skip to content

Commit 1345585

Browse files
authored
Merge pull request #927 from trevorbayless/custom_exceptions
Add specific move error exceptions
2 parents 9a81ad1 + a721c5b commit 1345585

3 files changed

Lines changed: 81 additions & 42 deletions

File tree

chess/__init__.py

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ def result(self) -> str:
165165
return "1/2-1/2" if self.winner is None else ("1-0" if self.winner else "0-1")
166166

167167

168+
class InvalidMoveError(ValueError):
169+
"""Raised when the attempted move is invalid in the current position"""
170+
171+
172+
class IllegalMoveError(ValueError):
173+
"""Raised when the attempted move is illegal in the current position"""
174+
175+
176+
class AmbiguousMoveError(ValueError):
177+
"""Raised when the attempted move is ambiguous in the current position"""
178+
179+
168180
Square = int
169181
SQUARES = [
170182
A1, B1, C1, D1, E1, F1, G1, H1,
@@ -551,23 +563,29 @@ def from_uci(cls, uci: str) -> Move:
551563
"""
552564
Parses a UCI string.
553565
554-
:raises: :exc:`ValueError` if the UCI string is invalid.
566+
:raises: :exc:`InvalidMoveError` if the UCI string is invalid.
555567
"""
556568
if uci == "0000":
557569
return cls.null()
558570
elif len(uci) == 4 and "@" == uci[1]:
559-
drop = PIECE_SYMBOLS.index(uci[0].lower())
560-
square = SQUARE_NAMES.index(uci[2:])
571+
try:
572+
drop = PIECE_SYMBOLS.index(uci[0].lower())
573+
square = SQUARE_NAMES.index(uci[2:])
574+
except ValueError:
575+
raise InvalidMoveError(f"invalid uci: {uci!r}")
561576
return cls(square, square, drop=drop)
562577
elif 4 <= len(uci) <= 5:
563-
from_square = SQUARE_NAMES.index(uci[0:2])
564-
to_square = SQUARE_NAMES.index(uci[2:4])
565-
promotion = PIECE_SYMBOLS.index(uci[4]) if len(uci) == 5 else None
578+
try:
579+
from_square = SQUARE_NAMES.index(uci[0:2])
580+
to_square = SQUARE_NAMES.index(uci[2:4])
581+
promotion = PIECE_SYMBOLS.index(uci[4]) if len(uci) == 5 else None
582+
except ValueError:
583+
raise InvalidMoveError(f"invalid uci: {uci!r}")
566584
if from_square == to_square:
567-
raise ValueError(f"invalid uci (use 0000 for null moves): {uci!r}")
585+
raise InvalidMoveError(f"invalid uci (use 0000 for null moves): {uci!r}")
568586
return cls(from_square, to_square, promotion=promotion)
569587
else:
570-
raise ValueError(f"expected uci string to be of length 4 or 5: {uci!r}")
588+
raise InvalidMoveError(f"expected uci string to be of length 4 or 5: {uci!r}")
571589

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

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

23172335
return move
23182336

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

29462964
for move in variation:
29472965
if not board.is_legal(move):
2948-
raise ValueError(f"illegal move {move} in position {board.fen()}")
2966+
raise IllegalMoveError(f"illegal move {move} in position {board.fen()}")
29492967

29502968
if board.turn == WHITE:
29512969
san.append(f"{board.fullmove_number}. {board.san_and_push(move)}")
@@ -2966,7 +2984,11 @@ def parse_san(self, san: str) -> Move:
29662984
29672985
The returned move is guaranteed to be either legal or a null move.
29682986
2969-
:raises: :exc:`ValueError` if the SAN is invalid, illegal or ambiguous.
2987+
:raises:
2988+
:exc:`ValueError` (or specifically an exception specified below) if the SAN is invalid, illegal or ambiguous.
2989+
- :exc:`InvalidMoveError` if the SAN is invalid.
2990+
- :exc:`IllegalMoveError` if the SAN is illegal.
2991+
- :exc:`AmbiguousMoveError` if the SAN is ambiguous.
29702992
"""
29712993
# Castling.
29722994
try:
@@ -2975,7 +2997,7 @@ def parse_san(self, san: str) -> Move:
29752997
elif san in ["O-O-O", "O-O-O+", "O-O-O#", "0-0-0", "0-0-0+", "0-0-0#"]:
29762998
return next(move for move in self.generate_castling_moves() if self.is_queenside_castling(move))
29772999
except StopIteration:
2978-
raise ValueError(f"illegal san: {san!r} in {self.fen()}")
3000+
raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}")
29793001

29803002
# Match normal moves.
29813003
match = SAN_REGEX.match(san)
@@ -2984,9 +3006,9 @@ def parse_san(self, san: str) -> Move:
29843006
if san in ["--", "Z0", "0000", "@@@@"]:
29853007
return Move.null()
29863008
elif "," in san:
2987-
raise ValueError(f"unsupported multi-leg move: {san!r}")
3009+
raise InvalidMoveError(f"unsupported multi-leg move: {san!r}")
29883010
else:
2989-
raise ValueError(f"invalid san: {san!r}")
3011+
raise InvalidMoveError(f"invalid san: {san!r}")
29903012

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

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

30333055
if matched_move:
3034-
raise ValueError(f"ambiguous san: {san!r} in {self.fen()}")
3056+
raise AmbiguousMoveError(f"ambiguous san: {san!r} in {self.fen()}")
30353057

30363058
matched_move = move
30373059

30383060
if not matched_move:
3039-
raise ValueError(f"illegal san: {san!r} in {self.fen()}")
3061+
raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}")
30403062

30413063
return matched_move
30423064

@@ -3047,7 +3069,11 @@ def push_san(self, san: str) -> Move:
30473069
30483070
Returns the move.
30493071
3050-
:raises: :exc:`ValueError` if neither legal nor a null move.
3072+
:raises:
3073+
:exc:`ValueError` (or specifically an exception specified below) if neither legal nor a null move.
3074+
- :exc:`InvalidMoveError` if the SAN is invalid.
3075+
- :exc:`IllegalMoveError` if the SAN is illegal.
3076+
- :exc:`AmbiguousMoveError` if the SAN is ambiguous.
30513077
"""
30523078
move = self.parse_san(san)
30533079
self.push(move)
@@ -3075,8 +3101,11 @@ def parse_uci(self, uci: str) -> Move:
30753101
30763102
The returned move is guaranteed to be either legal or a null move.
30773103
3078-
:raises: :exc:`ValueError` if the move is invalid or illegal in the
3104+
:raises:
3105+
:exc:`ValueError` (or specifically an exception specified below) if the move is invalid or illegal in the
30793106
current position (but not a null move).
3107+
- :exc:`InvalidMoveError` if the UCI is invalid.
3108+
- :exc:`IllegalMoveError` if the UCI is illegal.
30803109
"""
30813110
move = Move.from_uci(uci)
30823111

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

30893118
if not self.is_legal(move):
3090-
raise ValueError(f"illegal uci: {uci!r} in {self.fen()}")
3119+
raise IllegalMoveError(f"illegal uci: {uci!r} in {self.fen()}")
30913120

30923121
return move
30933122

@@ -3097,8 +3126,11 @@ def push_uci(self, uci: str) -> Move:
30973126
30983127
Returns the move.
30993128
3100-
:raises: :exc:`ValueError` if the move is invalid or illegal in the
3129+
:raises:
3130+
:exc:`ValueError` (or specifically an exception specified below) if the move is invalid or illegal in the
31013131
current position (but not a null move).
3132+
- :exc:`InvalidMoveError` if the UCI is invalid.
3133+
- :exc:`IllegalMoveError` if the UCI is illegal.
31023134
"""
31033135
move = self.parse_uci(uci)
31043136
self.push(move)

chess/variant.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ def parse_san(self, san: str) -> chess.Move:
989989
uci = "P" + uci
990990
move = chess.Move.from_uci(uci)
991991
if not self.is_legal(move):
992-
raise ValueError(f"illegal drop san: {san!r} in {self.fen()}")
992+
raise chess.IllegalMoveError(f"illegal drop san: {san!r} in {self.fen()}")
993993
return move
994994
else:
995995
return super().parse_san(san)

test.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,16 @@ def test_uci_parsing(self):
120120
self.assertEqual(chess.Move.from_uci("0000").uci(), "0000")
121121

122122
def test_invalid_uci(self):
123-
with self.assertRaises(ValueError):
123+
with self.assertRaises(chess.InvalidMoveError):
124124
chess.Move.from_uci("")
125125

126-
with self.assertRaises(ValueError):
126+
with self.assertRaises(chess.InvalidMoveError):
127127
chess.Move.from_uci("N")
128128

129-
with self.assertRaises(ValueError):
129+
with self.assertRaises(chess.InvalidMoveError):
130130
chess.Move.from_uci("z1g3")
131131

132-
with self.assertRaises(ValueError):
132+
with self.assertRaises(chess.InvalidMoveError):
133133
chess.Move.from_uci("Q@g9")
134134

135135
def test_xboard_move(self):
@@ -383,9 +383,9 @@ def test_castling(self):
383383
def test_castling_san(self):
384384
board = chess.Board("4k3/8/8/8/8/8/8/4K2R w K - 0 1")
385385
self.assertEqual(board.parse_san("O-O"), chess.Move.from_uci("e1g1"))
386-
with self.assertRaises(ValueError):
386+
with self.assertRaises(chess.IllegalMoveError):
387387
board.parse_san("Kg1")
388-
with self.assertRaises(ValueError):
388+
with self.assertRaises(chess.IllegalMoveError):
389389
board.parse_san("Kh1")
390390

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

505505
# Illegal moves.
506-
with self.assertRaises(ValueError):
506+
with self.assertRaises(chess.IllegalMoveError):
507507
board.find_move(chess.D2, chess.D8)
508-
with self.assertRaises(ValueError):
508+
with self.assertRaises(chess.IllegalMoveError):
509509
board.find_move(chess.E1, chess.A1)
510510

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

593+
def test_ambiguous_move(self):
594+
board = chess.Board("8/8/1n6/3R1P2/1n6/2k2K2/3p4/r6r b - - 0 82")
595+
with self.assertRaises(chess.AmbiguousMoveError):
596+
board.parse_san("Rf1")
597+
with self.assertRaises(chess.AmbiguousMoveError):
598+
board.parse_san("Nd5")
599+
593600
def test_scholars_mate(self):
594601
board = chess.Board()
595602

@@ -723,17 +730,17 @@ def test_lan(self):
723730

724731
def test_san_newline(self):
725732
board = chess.Board("rnbqk2r/ppppppbp/5np1/8/8/5NP1/PPPPPPBP/RNBQK2R w KQkq - 2 4")
726-
with self.assertRaises(ValueError):
733+
with self.assertRaises(chess.InvalidMoveError):
727734
board.parse_san("O-O\n")
728-
with self.assertRaises(ValueError):
735+
with self.assertRaises(chess.InvalidMoveError):
729736
board.parse_san("Nc3\n")
730737

731738
def test_pawn_capture_san_without_file(self):
732739
board = chess.Board("2rq1rk1/pb2bppp/1p2p3/n1ppPn2/2PP4/PP3N2/1B1NQPPP/RB3RK1 b - - 4 13")
733-
with self.assertRaises(ValueError):
740+
with self.assertRaises(chess.IllegalMoveError):
734741
board.parse_san("c4")
735742
board = chess.Board("4k3/8/8/4Pp2/8/8/8/4K3 w - f6 0 2")
736-
with self.assertRaises(ValueError):
743+
with self.assertRaises(chess.IllegalMoveError):
737744
board.parse_san("f6")
738745

739746
def test_variation_san(self):
@@ -763,7 +770,7 @@ def test_variation_san(self):
763770

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

40204027
# Capture is mandatory.
4021-
with self.assertRaises(ValueError):
4028+
with self.assertRaises(chess.IllegalMoveError):
40224029
board.push_san("Nf3")
40234030

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

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

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

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

41734180
def test_castling_rights_explode_with_king(self):
@@ -4495,7 +4502,7 @@ def test_capture_with_promotion(self):
44954502

44964503
def test_illegal_drop_uci(self):
44974504
board = chess.variant.CrazyhouseBoard()
4498-
with self.assertRaises(ValueError):
4505+
with self.assertRaises(chess.IllegalMoveError):
44994506
board.parse_uci("N@f3")
45004507

45014508
def test_crazyhouse_fen(self):

0 commit comments

Comments
 (0)