chessai.core.action

The core actions that agents are allowed to take. Default actions are provided, but custom actions can be easily created.

  1"""
  2The core actions that agents are allowed to take.
  3Default actions are provided, but custom actions can be easily created.
  4"""
  5
  6import re
  7import typing
  8
  9import edq.util.json
 10import edq.util.serial
 11
 12import chessai.core.coordinate
 13import chessai.core.piece
 14import chessai.core.types
 15
 16ACTION_PATTERN: re.Pattern = re.compile(r'^([a-z]+\d+)([a-z]+\d+)([a-zA-Z]?)$')
 17ACTION_KEY: str = 'actions'
 18
 19UCI_ACCEPT_DRAW_ACTION: str = 'Accept Draw'
 20UCI_FORFEIT_ACTION: str = 'Forfeit'
 21UCI_NULL_ACTION: str = '0000'
 22UCI_PROPOSE_DRAW_ACTION: str = 'Propose Draw'
 23UCI_REJECT_DRAW_ACTION: str = 'Reject Draw'
 24
 25class Action(edq.util.serial.DictConverter):
 26    """
 27    The base for all actions that an agent is allowed to take.
 28
 29    Actions are an immutable class, so do not change attributes after construction.
 30
 31    Actions are strings in the Chess UCI format
 32    (https://en.wikipedia.org/wiki/Universal_Chess_Interface).
 33    """
 34
 35    _type_order: typing.ClassVar[int]
 36    """
 37    The ordering of this action type.
 38    Child classes must choose a unique type number for their game.
 39    """
 40
 41    # This function is meant to be an abc.abstractmethod,
 42    # but for performance reasons it raises an error.
 43    def uci(self) -> str:
 44        """ Represent the agent action as a chess move in UCI format. """
 45
 46        raise NotImplementedError("Action.uci()")
 47
 48    # Comparing actions with non-action objects raises an error.
 49    def __lt__(self, other: object) -> bool:
 50        return bool(self._type_order < other._type_order)  # type: ignore[attr-defined]  # pylint: disable=no-member
 51
 52    # Comparing actions with non-action objects may raise an error.
 53    def __eq__(self, other: object) -> bool:
 54        return type(self) == type(other)
 55
 56    def __str__(self) -> str:
 57        return self.uci()
 58
 59    def __repr__(self) -> str:
 60        return self.uci()
 61
 62class NoneAction(Action):
 63    """ An action that signifies nothing happened. """
 64
 65    _type_order = 0
 66
 67    def uci(self) -> str:
 68        return UCI_NULL_ACTION
 69
 70class MoveAction(Action):
 71    """ Actions that move a piece from one coordinate to another. """
 72
 73    _type_order = 1
 74
 75    def __init__(self,
 76                 start_coordinate: chessai.core.coordinate.Coordinate,
 77                 end_coordinate: chessai.core.coordinate.Coordinate,
 78                 ) -> None:
 79        self.start_coordinate: chessai.core.coordinate.Coordinate = start_coordinate
 80        """ The starting coordinate for the action. """
 81
 82        self.end_coordinate: chessai.core.coordinate.Coordinate = end_coordinate
 83        """ The ending coordinate for the action. """
 84
 85    def uci(self) -> str:
 86        start = self.start_coordinate.uci()
 87        end = self.end_coordinate.uci()
 88
 89        return f"{start}{end}"
 90
 91    def __eq__(self, other: object) -> bool:
 92        if (type(self) != type(other)):
 93            return False
 94
 95        return (self.start_coordinate, self.end_coordinate) == (other.start_coordinate, other.end_coordinate) # type: ignore[attr-defined]  # pylint: disable=no-member
 96
 97    def __lt__(self, other: object) -> bool:
 98        if isinstance(other, MoveAction):
 99            return (self.start_coordinate, self.end_coordinate) < (other.start_coordinate, other.end_coordinate)
100
101        return bool(self._type_order < other._type_order)  # type: ignore[attr-defined]  # pylint: disable=no-member
102
103class PromotionAction(MoveAction):
104    """ Actions that move a piece and promote it to another piece type. """
105
106    _type_order = 2
107
108    def __init__(self,
109                 start_coordinate: chessai.core.coordinate.Coordinate,
110                 end_coordinate: chessai.core.coordinate.Coordinate,
111                 promotion: chessai.core.piece.Piece,
112                 ) -> None:
113        super().__init__(start_coordinate, end_coordinate)
114
115        self.promotion: chessai.core.piece.Piece = promotion
116        """ The piece to promote to, or None if this is not a promotion move. """
117
118    def uci(self) -> str:
119        movement_uci = super().uci()
120        promotion = self.promotion.symbol()
121
122        return f"{movement_uci}{promotion}"
123
124    def __eq__(self, other: object) -> bool:
125        if (not isinstance(other, PromotionAction)):
126            return False
127
128        return (self.start_coordinate, self.end_coordinate, self.promotion) == (other.start_coordinate, other.end_coordinate, other.promotion)
129
130    def __lt__(self, other: object) -> bool:
131        if isinstance(other, PromotionAction):
132            return (self.start_coordinate, self.end_coordinate, self.promotion) < (other.start_coordinate, other.end_coordinate, other.promotion)
133
134        return bool(self._type_order < other._type_order)  # type: ignore[attr-defined]  # pylint: disable=no-member
135
136class MetaAction(Action):
137    """
138    Meta actions for inter-agent communication regarding non-standard actions.
139
140    Meta actions include handling draws and forfeits.
141    """
142
143    def uci(self) -> str:
144        raise NotImplementedError("MetaAction.uci()")
145
146class ProposeDrawAction(MetaAction):
147    """ A meta action signaling the agent is offering a draw. """
148
149    _type_order = 3
150
151    def uci(self) -> str:
152        return UCI_PROPOSE_DRAW_ACTION
153
154class AcceptDrawAction(MetaAction):
155    """ A meta action responding to a draw proposal with an acceptance. """
156
157    _type_order = 4
158
159    def uci(self) -> str:
160        return UCI_ACCEPT_DRAW_ACTION
161
162class RejectDrawAction(MetaAction):
163    """ A meta action responding to a draw proposal with a rejection. """
164
165    _type_order = 5
166
167    def uci(self) -> str:
168        return UCI_REJECT_DRAW_ACTION
169
170class ForfeitAction(MetaAction):
171    """ A meta action signaling the agent is forfeiting the game. """
172
173    _type_order = 6
174
175    def uci(self) -> str:
176        return UCI_FORFEIT_ACTION
177
178def from_uci(uci: str) -> Action:
179    """
180    Parse a UCI string into the appropriate Action.
181    """
182
183    if (uci == UCI_NULL_ACTION):
184        return NoneAction()
185
186    if (uci == UCI_PROPOSE_DRAW_ACTION):
187        return ProposeDrawAction()
188
189    if (uci == UCI_ACCEPT_DRAW_ACTION):
190        return AcceptDrawAction()
191
192    if (uci == UCI_REJECT_DRAW_ACTION):
193        return RejectDrawAction()
194
195    if (uci == UCI_FORFEIT_ACTION):
196        return ForfeitAction()
197
198    match = ACTION_PATTERN.fullmatch(uci)
199    if (match is None):
200        raise ValueError("An action must be a pair of coordinates with an optional promotion piece:"
201                        + f" '{ACTION_PATTERN.pattern}', got: '{uci}'.")
202
203    start_coordinate = chessai.core.coordinate.Coordinate.from_uci(match.group(1))
204    end_coordinate = chessai.core.coordinate.Coordinate.from_uci(match.group(2))
205
206    tail = match.group(3)
207    if (len(tail) == 1):
208        if (tail not in chessai.core.piece.get_registered_piece_symbols()):
209            raise ValueError(f"Unknown promotion piece symbol '{tail}' in UCI string: '{uci}'.")
210
211        promotion = chessai.core.piece.get_registered_piece(tail)
212        return PromotionAction(start_coordinate, end_coordinate, promotion)
213
214    return MoveAction(start_coordinate, end_coordinate)
215
216def process_raw_action_list(raw_action_list: str | dict[str, typing.Any]) -> list[list[Action]]:
217    """ Convert raw move lines, from the CLI, into cleaned move lines. """
218
219    if (isinstance(raw_action_list, dict)):
220        raw_action_list = str(raw_action_list.get(ACTION_KEY, None))
221
222    parsed_move_lines = edq.util.json.loads(raw_action_list)
223
224    clean_move_lines: list[list[Action]] = []
225
226    for line in parsed_move_lines:
227        move_line = []
228        for uci in line:
229            move_line.append(from_uci(uci))
230
231        clean_move_lines.append(move_line)
232
233    return clean_move_lines
ACTION_PATTERN: re.Pattern = re.compile('^([a-z]+\\d+)([a-z]+\\d+)([a-zA-Z]?)$')
ACTION_KEY: str = 'actions'
UCI_ACCEPT_DRAW_ACTION: str = 'Accept Draw'
UCI_FORFEIT_ACTION: str = 'Forfeit'
UCI_NULL_ACTION: str = '0000'
UCI_PROPOSE_DRAW_ACTION: str = 'Propose Draw'
UCI_REJECT_DRAW_ACTION: str = 'Reject Draw'
class Action(edq.util.serial.DictConverter):
26class Action(edq.util.serial.DictConverter):
27    """
28    The base for all actions that an agent is allowed to take.
29
30    Actions are an immutable class, so do not change attributes after construction.
31
32    Actions are strings in the Chess UCI format
33    (https://en.wikipedia.org/wiki/Universal_Chess_Interface).
34    """
35
36    _type_order: typing.ClassVar[int]
37    """
38    The ordering of this action type.
39    Child classes must choose a unique type number for their game.
40    """
41
42    # This function is meant to be an abc.abstractmethod,
43    # but for performance reasons it raises an error.
44    def uci(self) -> str:
45        """ Represent the agent action as a chess move in UCI format. """
46
47        raise NotImplementedError("Action.uci()")
48
49    # Comparing actions with non-action objects raises an error.
50    def __lt__(self, other: object) -> bool:
51        return bool(self._type_order < other._type_order)  # type: ignore[attr-defined]  # pylint: disable=no-member
52
53    # Comparing actions with non-action objects may raise an error.
54    def __eq__(self, other: object) -> bool:
55        return type(self) == type(other)
56
57    def __str__(self) -> str:
58        return self.uci()
59
60    def __repr__(self) -> str:
61        return self.uci()

The base for all actions that an agent is allowed to take.

Actions are an immutable class, so do not change attributes after construction.

Actions are strings in the Chess UCI format (https://en.wikipedia.org/wiki/Universal_Chess_Interface).

def uci(self) -> str:
44    def uci(self) -> str:
45        """ Represent the agent action as a chess move in UCI format. """
46
47        raise NotImplementedError("Action.uci()")

Represent the agent action as a chess move in UCI format.

class NoneAction(Action):
63class NoneAction(Action):
64    """ An action that signifies nothing happened. """
65
66    _type_order = 0
67
68    def uci(self) -> str:
69        return UCI_NULL_ACTION

An action that signifies nothing happened.

def uci(self) -> str:
68    def uci(self) -> str:
69        return UCI_NULL_ACTION

Represent the agent action as a chess move in UCI format.

class MoveAction(Action):
 71class MoveAction(Action):
 72    """ Actions that move a piece from one coordinate to another. """
 73
 74    _type_order = 1
 75
 76    def __init__(self,
 77                 start_coordinate: chessai.core.coordinate.Coordinate,
 78                 end_coordinate: chessai.core.coordinate.Coordinate,
 79                 ) -> None:
 80        self.start_coordinate: chessai.core.coordinate.Coordinate = start_coordinate
 81        """ The starting coordinate for the action. """
 82
 83        self.end_coordinate: chessai.core.coordinate.Coordinate = end_coordinate
 84        """ The ending coordinate for the action. """
 85
 86    def uci(self) -> str:
 87        start = self.start_coordinate.uci()
 88        end = self.end_coordinate.uci()
 89
 90        return f"{start}{end}"
 91
 92    def __eq__(self, other: object) -> bool:
 93        if (type(self) != type(other)):
 94            return False
 95
 96        return (self.start_coordinate, self.end_coordinate) == (other.start_coordinate, other.end_coordinate) # type: ignore[attr-defined]  # pylint: disable=no-member
 97
 98    def __lt__(self, other: object) -> bool:
 99        if isinstance(other, MoveAction):
100            return (self.start_coordinate, self.end_coordinate) < (other.start_coordinate, other.end_coordinate)
101
102        return bool(self._type_order < other._type_order)  # type: ignore[attr-defined]  # pylint: disable=no-member

Actions that move a piece from one coordinate to another.

MoveAction( start_coordinate: chessai.core.coordinate.Coordinate, end_coordinate: chessai.core.coordinate.Coordinate)
76    def __init__(self,
77                 start_coordinate: chessai.core.coordinate.Coordinate,
78                 end_coordinate: chessai.core.coordinate.Coordinate,
79                 ) -> None:
80        self.start_coordinate: chessai.core.coordinate.Coordinate = start_coordinate
81        """ The starting coordinate for the action. """
82
83        self.end_coordinate: chessai.core.coordinate.Coordinate = end_coordinate
84        """ The ending coordinate for the action. """

The starting coordinate for the action.

The ending coordinate for the action.

def uci(self) -> str:
86    def uci(self) -> str:
87        start = self.start_coordinate.uci()
88        end = self.end_coordinate.uci()
89
90        return f"{start}{end}"

Represent the agent action as a chess move in UCI format.

class PromotionAction(MoveAction):
104class PromotionAction(MoveAction):
105    """ Actions that move a piece and promote it to another piece type. """
106
107    _type_order = 2
108
109    def __init__(self,
110                 start_coordinate: chessai.core.coordinate.Coordinate,
111                 end_coordinate: chessai.core.coordinate.Coordinate,
112                 promotion: chessai.core.piece.Piece,
113                 ) -> None:
114        super().__init__(start_coordinate, end_coordinate)
115
116        self.promotion: chessai.core.piece.Piece = promotion
117        """ The piece to promote to, or None if this is not a promotion move. """
118
119    def uci(self) -> str:
120        movement_uci = super().uci()
121        promotion = self.promotion.symbol()
122
123        return f"{movement_uci}{promotion}"
124
125    def __eq__(self, other: object) -> bool:
126        if (not isinstance(other, PromotionAction)):
127            return False
128
129        return (self.start_coordinate, self.end_coordinate, self.promotion) == (other.start_coordinate, other.end_coordinate, other.promotion)
130
131    def __lt__(self, other: object) -> bool:
132        if isinstance(other, PromotionAction):
133            return (self.start_coordinate, self.end_coordinate, self.promotion) < (other.start_coordinate, other.end_coordinate, other.promotion)
134
135        return bool(self._type_order < other._type_order)  # type: ignore[attr-defined]  # pylint: disable=no-member

Actions that move a piece and promote it to another piece type.

PromotionAction( start_coordinate: chessai.core.coordinate.Coordinate, end_coordinate: chessai.core.coordinate.Coordinate, promotion: chessai.core.piece.Piece)
109    def __init__(self,
110                 start_coordinate: chessai.core.coordinate.Coordinate,
111                 end_coordinate: chessai.core.coordinate.Coordinate,
112                 promotion: chessai.core.piece.Piece,
113                 ) -> None:
114        super().__init__(start_coordinate, end_coordinate)
115
116        self.promotion: chessai.core.piece.Piece = promotion
117        """ The piece to promote to, or None if this is not a promotion move. """

The piece to promote to, or None if this is not a promotion move.

def uci(self) -> str:
119    def uci(self) -> str:
120        movement_uci = super().uci()
121        promotion = self.promotion.symbol()
122
123        return f"{movement_uci}{promotion}"

Represent the agent action as a chess move in UCI format.

class MetaAction(Action):
137class MetaAction(Action):
138    """
139    Meta actions for inter-agent communication regarding non-standard actions.
140
141    Meta actions include handling draws and forfeits.
142    """
143
144    def uci(self) -> str:
145        raise NotImplementedError("MetaAction.uci()")

Meta actions for inter-agent communication regarding non-standard actions.

Meta actions include handling draws and forfeits.

def uci(self) -> str:
144    def uci(self) -> str:
145        raise NotImplementedError("MetaAction.uci()")

Represent the agent action as a chess move in UCI format.

class ProposeDrawAction(MetaAction):
147class ProposeDrawAction(MetaAction):
148    """ A meta action signaling the agent is offering a draw. """
149
150    _type_order = 3
151
152    def uci(self) -> str:
153        return UCI_PROPOSE_DRAW_ACTION

A meta action signaling the agent is offering a draw.

def uci(self) -> str:
152    def uci(self) -> str:
153        return UCI_PROPOSE_DRAW_ACTION

Represent the agent action as a chess move in UCI format.

class AcceptDrawAction(MetaAction):
155class AcceptDrawAction(MetaAction):
156    """ A meta action responding to a draw proposal with an acceptance. """
157
158    _type_order = 4
159
160    def uci(self) -> str:
161        return UCI_ACCEPT_DRAW_ACTION

A meta action responding to a draw proposal with an acceptance.

def uci(self) -> str:
160    def uci(self) -> str:
161        return UCI_ACCEPT_DRAW_ACTION

Represent the agent action as a chess move in UCI format.

class RejectDrawAction(MetaAction):
163class RejectDrawAction(MetaAction):
164    """ A meta action responding to a draw proposal with a rejection. """
165
166    _type_order = 5
167
168    def uci(self) -> str:
169        return UCI_REJECT_DRAW_ACTION

A meta action responding to a draw proposal with a rejection.

def uci(self) -> str:
168    def uci(self) -> str:
169        return UCI_REJECT_DRAW_ACTION

Represent the agent action as a chess move in UCI format.

class ForfeitAction(MetaAction):
171class ForfeitAction(MetaAction):
172    """ A meta action signaling the agent is forfeiting the game. """
173
174    _type_order = 6
175
176    def uci(self) -> str:
177        return UCI_FORFEIT_ACTION

A meta action signaling the agent is forfeiting the game.

def uci(self) -> str:
176    def uci(self) -> str:
177        return UCI_FORFEIT_ACTION

Represent the agent action as a chess move in UCI format.

def from_uci(uci: str) -> Action:
179def from_uci(uci: str) -> Action:
180    """
181    Parse a UCI string into the appropriate Action.
182    """
183
184    if (uci == UCI_NULL_ACTION):
185        return NoneAction()
186
187    if (uci == UCI_PROPOSE_DRAW_ACTION):
188        return ProposeDrawAction()
189
190    if (uci == UCI_ACCEPT_DRAW_ACTION):
191        return AcceptDrawAction()
192
193    if (uci == UCI_REJECT_DRAW_ACTION):
194        return RejectDrawAction()
195
196    if (uci == UCI_FORFEIT_ACTION):
197        return ForfeitAction()
198
199    match = ACTION_PATTERN.fullmatch(uci)
200    if (match is None):
201        raise ValueError("An action must be a pair of coordinates with an optional promotion piece:"
202                        + f" '{ACTION_PATTERN.pattern}', got: '{uci}'.")
203
204    start_coordinate = chessai.core.coordinate.Coordinate.from_uci(match.group(1))
205    end_coordinate = chessai.core.coordinate.Coordinate.from_uci(match.group(2))
206
207    tail = match.group(3)
208    if (len(tail) == 1):
209        if (tail not in chessai.core.piece.get_registered_piece_symbols()):
210            raise ValueError(f"Unknown promotion piece symbol '{tail}' in UCI string: '{uci}'.")
211
212        promotion = chessai.core.piece.get_registered_piece(tail)
213        return PromotionAction(start_coordinate, end_coordinate, promotion)
214
215    return MoveAction(start_coordinate, end_coordinate)

Parse a UCI string into the appropriate Action.

def process_raw_action_list( raw_action_list: str | dict[str, typing.Any]) -> list[list[Action]]:
217def process_raw_action_list(raw_action_list: str | dict[str, typing.Any]) -> list[list[Action]]:
218    """ Convert raw move lines, from the CLI, into cleaned move lines. """
219
220    if (isinstance(raw_action_list, dict)):
221        raw_action_list = str(raw_action_list.get(ACTION_KEY, None))
222
223    parsed_move_lines = edq.util.json.loads(raw_action_list)
224
225    clean_move_lines: list[list[Action]] = []
226
227    for line in parsed_move_lines:
228        move_line = []
229        for uci in line:
230            move_line.append(from_uci(uci))
231
232        clean_move_lines.append(move_line)
233
234    return clean_move_lines

Convert raw move lines, from the CLI, into cleaned move lines.