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
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).
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.
Inherited Members
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.
Inherited Members
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.
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. """
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.
Inherited Members
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.
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.
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.
Inherited Members
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.
Inherited Members
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.
Inherited Members
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.
Inherited Members
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.
Inherited Members
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.
Inherited Members
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.
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.