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)
COORDINATES_KEY: str = 'coordinates'
COORDINATE_PATTERN: re.Pattern = re.compile('([a-z]+)(\\d+)')
class Coordinate(edq.util.serial.DictConverter):
 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).

Coordinate(file: int, rank: int)
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. """
file: int

The file of the coordinate.

rank: int

The rank of the coordinate.

@classmethod
def from_uci(cls, uci: str) -> Coordinate:
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.

def uci(self, only_file: bool = False, only_rank: bool = False) -> str:
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.

def offset( self, file_delta: int, rank_delta: int) -> 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.

def file_distance(self, other: Coordinate) -> int:
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.

def rank_distance(self, other: Coordinate) -> int:
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.

def chebyshev_distance(self, other: Coordinate) -> int:
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.

def manhattan_distance(self, other: Coordinate) -> int:
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.

@classmethod
def from_flat_dict(cls, data: dict[str, typing.Any]) -> Coordinate:
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.

def coordinates_from_dict(data: dict[str, typing.Any]) -> list[Coordinate]:
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.

NULL_COORDINATE = (-1, -1)