chessai.core.coordinate
1import re 2import typing 3 4import edq.util.serial 5 6COORDINATES_KEY: str = 'coordinates' 7 8COORDINATE_PATTERN: re.Pattern = re.compile(r'([a-z]+)(\d+)') 9 10class Coordinate(edq.util.serial.DictConverter): 11 """ 12 An immutable chess coordinate on a board. 13 14 File increases left-to-right (a=0, h=7). 15 Rank increases bottom-to-top (1=0, 8=7). 16 """ 17 18 def __init__(self, file: int, rank: int) -> None: 19 self.file: int = file 20 """ The file of the coordinate. """ 21 22 self.rank: int = rank 23 """ The rank of the coordinate. """ 24 25 def __eq__(self, other: object) -> bool: 26 if (not isinstance(other, Coordinate)): 27 return False 28 29 return (self.file == other.file) and (self.rank == other.rank) 30 31 def __lt__(self, other: object) -> bool: 32 if (not isinstance(other, Coordinate)): 33 return False 34 35 if (self.file < other.file): 36 return True 37 38 if (self.file == other.file): 39 return self.rank < other.rank 40 41 return False 42 43 def __hash__(self) -> int: 44 return hash((self.file, self.rank)) 45 46 @classmethod 47 def from_uci(cls, uci: str) -> 'Coordinate': 48 """ Construct a Coordinate from standard algebraic coordinate name. """ 49 50 match = re.fullmatch(COORDINATE_PATTERN, uci.lower()) 51 if (match is None): 52 raise ValueError(f"Improper Coordinate name: '{uci}', expected '[a-z]+\\d+'.") 53 54 raw_file, raw_rank = match.groups() 55 56 # Convert base-26 string into an integer. 57 file = 0 58 for char in raw_file: 59 file = (file * 26) + (ord(char) - ord('a') + 1) 60 61 # Adjust the file to be 0-indexed. 62 file -= 1 63 64 # Adjust the rank to be 0-indexed. 65 rank = int(raw_rank) - 1 66 67 return cls(file, rank) 68 69 def uci(self, only_file: bool = False, only_rank: bool = False) -> str: 70 """ The standard algebraic name of this coordinate. """ 71 72 # Convert the integer coordinate to a base-26 string, switching to the 1-indexed format. 73 file_num = self.file + 1 74 file_chars = [] 75 76 while file_num > 0: 77 file_num -= 1 78 file_chars.append(chr(file_num % 26 + ord('a'))) 79 file_num //= 26 80 81 file_str = ''.join(reversed(file_chars)) 82 rank_str = str(self.rank + 1) 83 84 if (only_file): 85 return file_str 86 87 if (only_rank): 88 return rank_str 89 90 return f"{file_str}{rank_str}" 91 92 def offset(self, file_delta: int, rank_delta: int) -> 'Coordinate': 93 """ Return the coordinate reached by moving (file_delta, rank_delta) from this coordinate. """ 94 95 new_file = self.file + file_delta 96 new_rank = self.rank + rank_delta 97 98 return Coordinate(new_file, new_rank) 99 100 def file_distance(self, other: 'Coordinate') -> int: 101 """ The absolute difference in file between this coordinate and another. """ 102 103 return abs(self.file - other.file) 104 105 def rank_distance(self, other: 'Coordinate') -> int: 106 """ The absolute difference in rank between this coordinate and another. """ 107 108 return abs(self.rank - other.rank) 109 110 def chebyshev_distance(self, other: 'Coordinate') -> int: 111 """ 112 The Chebyshev distance (chessboard distance) between two coordinates. 113 This is the minimum number of king moves to travel between them. 114 115 Useful as a building block for heuristics. 116 """ 117 118 return max(self.file_distance(other), self.rank_distance(other)) 119 120 def manhattan_distance(self, other: 'Coordinate') -> int: 121 """ 122 The Manhattan distance between two coordinates. 123 Note: this is NOT an admissible heuristic for knight movement, 124 since a knight does not move in cardinal directions. 125 Students should think carefully before using this in their heuristic. 126 """ 127 128 return self.file_distance(other) + self.rank_distance(other) 129 130 def __str__(self) -> str: 131 return f"({self.file}, {self.rank})" 132 133 def __repr__(self) -> str: 134 return str(self) 135 136 @classmethod 137 def from_flat_dict(cls, data: dict[str, typing.Any]) -> 'Coordinate': 138 """ Create a coordinate from a dictionary of data. """ 139 140 return Coordinate(file = data['file'], rank = data['rank']) 141 142def coordinates_from_dict(data: dict[str, typing.Any]) -> list[Coordinate]: 143 """ 144 Get a list of coordinates from a dict. 145 The 'coordinates' key will be checked. 146 """ 147 148 clean_coordinates: list[Coordinate] = [] 149 150 raw_coordinates = data.get(COORDINATES_KEY, []) 151 if (isinstance(raw_coordinates, str)): 152 raw_coordinates = raw_coordinates.split(',') 153 154 for raw_coordinate in raw_coordinates: 155 clean_coordinate = None 156 157 if (isinstance(raw_coordinate, dict)): 158 clean_coordinate = Coordinate.from_flat_dict(raw_coordinate) 159 elif (isinstance(raw_coordinate, str)): 160 clean_coordinate = Coordinate.from_uci(raw_coordinate) 161 162 if (clean_coordinate is None): 163 continue 164 165 clean_coordinates.append(clean_coordinate) 166 167 return clean_coordinates 168 169NULL_COORDINATE = Coordinate(-1, -1)
11class Coordinate(edq.util.serial.DictConverter): 12 """ 13 An immutable chess coordinate on a board. 14 15 File increases left-to-right (a=0, h=7). 16 Rank increases bottom-to-top (1=0, 8=7). 17 """ 18 19 def __init__(self, file: int, rank: int) -> None: 20 self.file: int = file 21 """ The file of the coordinate. """ 22 23 self.rank: int = rank 24 """ The rank of the coordinate. """ 25 26 def __eq__(self, other: object) -> bool: 27 if (not isinstance(other, Coordinate)): 28 return False 29 30 return (self.file == other.file) and (self.rank == other.rank) 31 32 def __lt__(self, other: object) -> bool: 33 if (not isinstance(other, Coordinate)): 34 return False 35 36 if (self.file < other.file): 37 return True 38 39 if (self.file == other.file): 40 return self.rank < other.rank 41 42 return False 43 44 def __hash__(self) -> int: 45 return hash((self.file, self.rank)) 46 47 @classmethod 48 def from_uci(cls, uci: str) -> 'Coordinate': 49 """ Construct a Coordinate from standard algebraic coordinate name. """ 50 51 match = re.fullmatch(COORDINATE_PATTERN, uci.lower()) 52 if (match is None): 53 raise ValueError(f"Improper Coordinate name: '{uci}', expected '[a-z]+\\d+'.") 54 55 raw_file, raw_rank = match.groups() 56 57 # Convert base-26 string into an integer. 58 file = 0 59 for char in raw_file: 60 file = (file * 26) + (ord(char) - ord('a') + 1) 61 62 # Adjust the file to be 0-indexed. 63 file -= 1 64 65 # Adjust the rank to be 0-indexed. 66 rank = int(raw_rank) - 1 67 68 return cls(file, rank) 69 70 def uci(self, only_file: bool = False, only_rank: bool = False) -> str: 71 """ The standard algebraic name of this coordinate. """ 72 73 # Convert the integer coordinate to a base-26 string, switching to the 1-indexed format. 74 file_num = self.file + 1 75 file_chars = [] 76 77 while file_num > 0: 78 file_num -= 1 79 file_chars.append(chr(file_num % 26 + ord('a'))) 80 file_num //= 26 81 82 file_str = ''.join(reversed(file_chars)) 83 rank_str = str(self.rank + 1) 84 85 if (only_file): 86 return file_str 87 88 if (only_rank): 89 return rank_str 90 91 return f"{file_str}{rank_str}" 92 93 def offset(self, file_delta: int, rank_delta: int) -> 'Coordinate': 94 """ Return the coordinate reached by moving (file_delta, rank_delta) from this coordinate. """ 95 96 new_file = self.file + file_delta 97 new_rank = self.rank + rank_delta 98 99 return Coordinate(new_file, new_rank) 100 101 def file_distance(self, other: 'Coordinate') -> int: 102 """ The absolute difference in file between this coordinate and another. """ 103 104 return abs(self.file - other.file) 105 106 def rank_distance(self, other: 'Coordinate') -> int: 107 """ The absolute difference in rank between this coordinate and another. """ 108 109 return abs(self.rank - other.rank) 110 111 def chebyshev_distance(self, other: 'Coordinate') -> int: 112 """ 113 The Chebyshev distance (chessboard distance) between two coordinates. 114 This is the minimum number of king moves to travel between them. 115 116 Useful as a building block for heuristics. 117 """ 118 119 return max(self.file_distance(other), self.rank_distance(other)) 120 121 def manhattan_distance(self, other: 'Coordinate') -> int: 122 """ 123 The Manhattan distance between two coordinates. 124 Note: this is NOT an admissible heuristic for knight movement, 125 since a knight does not move in cardinal directions. 126 Students should think carefully before using this in their heuristic. 127 """ 128 129 return self.file_distance(other) + self.rank_distance(other) 130 131 def __str__(self) -> str: 132 return f"({self.file}, {self.rank})" 133 134 def __repr__(self) -> str: 135 return str(self) 136 137 @classmethod 138 def from_flat_dict(cls, data: dict[str, typing.Any]) -> 'Coordinate': 139 """ Create a coordinate from a dictionary of data. """ 140 141 return Coordinate(file = data['file'], rank = data['rank'])
An immutable chess coordinate on a board.
File increases left-to-right (a=0, h=7). Rank increases bottom-to-top (1=0, 8=7).
47 @classmethod 48 def from_uci(cls, uci: str) -> 'Coordinate': 49 """ Construct a Coordinate from standard algebraic coordinate name. """ 50 51 match = re.fullmatch(COORDINATE_PATTERN, uci.lower()) 52 if (match is None): 53 raise ValueError(f"Improper Coordinate name: '{uci}', expected '[a-z]+\\d+'.") 54 55 raw_file, raw_rank = match.groups() 56 57 # Convert base-26 string into an integer. 58 file = 0 59 for char in raw_file: 60 file = (file * 26) + (ord(char) - ord('a') + 1) 61 62 # Adjust the file to be 0-indexed. 63 file -= 1 64 65 # Adjust the rank to be 0-indexed. 66 rank = int(raw_rank) - 1 67 68 return cls(file, rank)
Construct a Coordinate from standard algebraic coordinate name.
70 def uci(self, only_file: bool = False, only_rank: bool = False) -> str: 71 """ The standard algebraic name of this coordinate. """ 72 73 # Convert the integer coordinate to a base-26 string, switching to the 1-indexed format. 74 file_num = self.file + 1 75 file_chars = [] 76 77 while file_num > 0: 78 file_num -= 1 79 file_chars.append(chr(file_num % 26 + ord('a'))) 80 file_num //= 26 81 82 file_str = ''.join(reversed(file_chars)) 83 rank_str = str(self.rank + 1) 84 85 if (only_file): 86 return file_str 87 88 if (only_rank): 89 return rank_str 90 91 return f"{file_str}{rank_str}"
The standard algebraic name of this coordinate.
93 def offset(self, file_delta: int, rank_delta: int) -> 'Coordinate': 94 """ Return the coordinate reached by moving (file_delta, rank_delta) from this coordinate. """ 95 96 new_file = self.file + file_delta 97 new_rank = self.rank + rank_delta 98 99 return Coordinate(new_file, new_rank)
Return the coordinate reached by moving (file_delta, rank_delta) from this coordinate.
101 def file_distance(self, other: 'Coordinate') -> int: 102 """ The absolute difference in file between this coordinate and another. """ 103 104 return abs(self.file - other.file)
The absolute difference in file between this coordinate and another.
106 def rank_distance(self, other: 'Coordinate') -> int: 107 """ The absolute difference in rank between this coordinate and another. """ 108 109 return abs(self.rank - other.rank)
The absolute difference in rank between this coordinate and another.
111 def chebyshev_distance(self, other: 'Coordinate') -> int: 112 """ 113 The Chebyshev distance (chessboard distance) between two coordinates. 114 This is the minimum number of king moves to travel between them. 115 116 Useful as a building block for heuristics. 117 """ 118 119 return max(self.file_distance(other), self.rank_distance(other))
The Chebyshev distance (chessboard distance) between two coordinates. This is the minimum number of king moves to travel between them.
Useful as a building block for heuristics.
121 def manhattan_distance(self, other: 'Coordinate') -> int: 122 """ 123 The Manhattan distance between two coordinates. 124 Note: this is NOT an admissible heuristic for knight movement, 125 since a knight does not move in cardinal directions. 126 Students should think carefully before using this in their heuristic. 127 """ 128 129 return self.file_distance(other) + self.rank_distance(other)
The Manhattan distance between two coordinates. Note: this is NOT an admissible heuristic for knight movement, since a knight does not move in cardinal directions. Students should think carefully before using this in their heuristic.
137 @classmethod 138 def from_flat_dict(cls, data: dict[str, typing.Any]) -> 'Coordinate': 139 """ Create a coordinate from a dictionary of data. """ 140 141 return Coordinate(file = data['file'], rank = data['rank'])
Create a coordinate from a dictionary of data.
Inherited Members
143def coordinates_from_dict(data: dict[str, typing.Any]) -> list[Coordinate]: 144 """ 145 Get a list of coordinates from a dict. 146 The 'coordinates' key will be checked. 147 """ 148 149 clean_coordinates: list[Coordinate] = [] 150 151 raw_coordinates = data.get(COORDINATES_KEY, []) 152 if (isinstance(raw_coordinates, str)): 153 raw_coordinates = raw_coordinates.split(',') 154 155 for raw_coordinate in raw_coordinates: 156 clean_coordinate = None 157 158 if (isinstance(raw_coordinate, dict)): 159 clean_coordinate = Coordinate.from_flat_dict(raw_coordinate) 160 elif (isinstance(raw_coordinate, str)): 161 clean_coordinate = Coordinate.from_uci(raw_coordinate) 162 163 if (clean_coordinate is None): 164 continue 165 166 clean_coordinates.append(clean_coordinate) 167 168 return clean_coordinates
Get a list of coordinates from a dict. The 'coordinates' key will be checked.