chessai.ui.text

  1import sys
  2import typing
  3
  4import chessai.core.gamestate
  5import chessai.core.piece
  6import chessai.core.ui
  7
  8DEFAULT_PADDING_SIZE: int = 1
  9""" The default number of padding spaces between each piece and the square border. """
 10
 11class TextUI(chessai.core.ui.UI):
 12    """
 13    A simple UI that renders the game to a text stream.
 14    This UI will be simple and generally meant for debugging.
 15    """
 16
 17    SYMBOL_SIZE: int = 2
 18    """
 19    The space reserved to display the symbols.
 20    Pieces represent themselves as unicode symbols, which often take two spaces.
 21    """
 22
 23    def __init__(self,
 24            output_stream: typing.TextIO,
 25            padding_size: int = DEFAULT_PADDING_SIZE,
 26            **kwargs: typing.Any) -> None:
 27        super().__init__(**kwargs)
 28
 29        self._output_stream: typing.TextIO = output_stream
 30        """ The stream output will be sent to. """
 31
 32        self._padding_size: int = padding_size
 33        """ The number of padding spaces between each piece and the square border. """
 34
 35        self._cell_width: int = self.SYMBOL_SIZE
 36        """ The width of an individual cell, including the padding and symbol sizes. """
 37
 38        self._file_labels: str | None = None
 39        """ The file labels to be displayed at the bottom of the board. """
 40
 41        self._horizontal_border: str | None = None
 42        """ The horizontal border to be displayed between each rank. """
 43
 44    def draw(self,
 45            state: chessai.core.gamestate.GameState,
 46            termination_reason: chessai.core.types.TerminationReason | None = None,
 47            **kwargs: typing.Any,
 48            ) -> None:
 49        if (state.get_previous_action() == chessai.core.action.NoneAction()):
 50            return
 51
 52        # Draw the top border.
 53        self._output_stream.write(self._get_horizontal_border(state))
 54
 55        prefix_padding_size = self._get_prefix_padding_size(state)
 56
 57        for rank in range((state.board.num_ranks - 1), -1, -1):
 58            rank_str = str(rank + 1)
 59            rank_padding_size = prefix_padding_size - len(rank_str)
 60            rank_padding = ' ' * rank_padding_size
 61
 62            line = f"{rank_str}{rank_padding}{self._get_padding()}|"
 63
 64            for file in range(state.board.num_files):
 65                piece_symbol = self._translate_piece(state.board.get(file, rank))
 66                line += f"{piece_symbol}|"
 67
 68            self._output_stream.write(line + "\n")
 69            self._output_stream.write(self._get_horizontal_border(state))
 70
 71        self._output_stream.write(self._get_file_labels(state))
 72
 73        # Display the most recent action.
 74        action = state.get_previous_action()
 75        if (action is not None):
 76            self._output_stream.write(f"Previous action: '{action}'.\n")
 77
 78        if (termination_reason is not None):
 79            self._output_stream.write('Game Over!\n')
 80            self._output_stream.write(f"Termination Reason: '{termination_reason}'.\n")
 81
 82        self._output_stream.write('\n')
 83        self._output_stream.flush()
 84
 85    def _get_padding(self) -> str:
 86        """ Get the padding for this UI. """
 87
 88        return " " * self._padding_size
 89
 90    def _get_prefix_padding(self, state: chessai.core.gamestate.GameState) -> str:
 91        """
 92        Get the initial padding for rows without a column numbering,
 93        such as the horizontal border and the file labels.
 94        """
 95
 96        prefix_padding_size = self._get_prefix_padding_size(state)
 97
 98        prefix_padding = ' ' * prefix_padding_size
 99
100        return f"{prefix_padding}{self._get_padding()}"
101
102
103    def _get_prefix_padding_size(self, state: chessai.core.gamestate.GameState) -> int:
104        """ Get the size of the prefix padding to account for the rank labels. """
105
106        prefix_padding_size = 1
107        num_ranks = state.board.num_ranks
108
109        while (num_ranks >= 10):
110            prefix_padding_size += 1
111            num_ranks = num_ranks // 10
112
113        return prefix_padding_size
114
115    def _get_horizontal_border(self, state: chessai.core.gamestate.GameState) -> str:
116        """ Get the horizontal border for the UI. """
117
118        if (self._horizontal_border is not None):
119            return self._horizontal_border
120
121        dash_segment = '-' * self._cell_width + '+'
122
123        horizontal_border = f"{self._get_prefix_padding(state)}+" + (dash_segment * state.board.num_files) + '\n'
124
125        # Cache the horizontal border for subsequent calls.
126        self._horizontal_border = horizontal_border
127
128        return self._horizontal_border
129
130    def _get_file_labels(self, state: chessai.core.gamestate.GameState) -> str:
131        """ Get the labels of the files for the bottom of the board. """
132
133        if (self._file_labels is not None):
134            return self._file_labels
135
136        file_labels = self._get_prefix_padding(state)
137        for file in range(state.board.num_files):
138            # Use a coordinate to get the file name.
139            coordinate = chessai.core.coordinate.Coordinate(file, 0)
140
141            file_str = coordinate.uci(only_file = True)
142            file_labels += f"{self._get_padding()}{file_str}{self._get_padding()}"
143
144        file_labels += '\n'
145
146        # Cache the file labels for subsequent calls to the UI.
147        self._file_labels = file_labels
148
149        return file_labels
150
151    def _translate_piece(self, piece: chessai.core.piece.Piece | None) -> str:
152        """
153        Convert a piece to a string.
154        This can be trivial (since a piece knows how to represent itself as a string),
155        but this allows children to implement special conversions.
156        """
157
158        if (piece is None):
159            return ' ' * self.SYMBOL_SIZE
160
161        return piece.unicode_symbol().center(self.SYMBOL_SIZE)
162
163class StdioUI(TextUI):
164    """
165    A convenience class for a TextUI using stdout.
166    """
167
168    def __init__(self, **kwargs: typing.Any) -> None:
169        super().__init__(sys.stdout, **kwargs)
DEFAULT_PADDING_SIZE: int = 1

The default number of padding spaces between each piece and the square border.

class TextUI(chessai.core.ui.UI):
 12class TextUI(chessai.core.ui.UI):
 13    """
 14    A simple UI that renders the game to a text stream.
 15    This UI will be simple and generally meant for debugging.
 16    """
 17
 18    SYMBOL_SIZE: int = 2
 19    """
 20    The space reserved to display the symbols.
 21    Pieces represent themselves as unicode symbols, which often take two spaces.
 22    """
 23
 24    def __init__(self,
 25            output_stream: typing.TextIO,
 26            padding_size: int = DEFAULT_PADDING_SIZE,
 27            **kwargs: typing.Any) -> None:
 28        super().__init__(**kwargs)
 29
 30        self._output_stream: typing.TextIO = output_stream
 31        """ The stream output will be sent to. """
 32
 33        self._padding_size: int = padding_size
 34        """ The number of padding spaces between each piece and the square border. """
 35
 36        self._cell_width: int = self.SYMBOL_SIZE
 37        """ The width of an individual cell, including the padding and symbol sizes. """
 38
 39        self._file_labels: str | None = None
 40        """ The file labels to be displayed at the bottom of the board. """
 41
 42        self._horizontal_border: str | None = None
 43        """ The horizontal border to be displayed between each rank. """
 44
 45    def draw(self,
 46            state: chessai.core.gamestate.GameState,
 47            termination_reason: chessai.core.types.TerminationReason | None = None,
 48            **kwargs: typing.Any,
 49            ) -> None:
 50        if (state.get_previous_action() == chessai.core.action.NoneAction()):
 51            return
 52
 53        # Draw the top border.
 54        self._output_stream.write(self._get_horizontal_border(state))
 55
 56        prefix_padding_size = self._get_prefix_padding_size(state)
 57
 58        for rank in range((state.board.num_ranks - 1), -1, -1):
 59            rank_str = str(rank + 1)
 60            rank_padding_size = prefix_padding_size - len(rank_str)
 61            rank_padding = ' ' * rank_padding_size
 62
 63            line = f"{rank_str}{rank_padding}{self._get_padding()}|"
 64
 65            for file in range(state.board.num_files):
 66                piece_symbol = self._translate_piece(state.board.get(file, rank))
 67                line += f"{piece_symbol}|"
 68
 69            self._output_stream.write(line + "\n")
 70            self._output_stream.write(self._get_horizontal_border(state))
 71
 72        self._output_stream.write(self._get_file_labels(state))
 73
 74        # Display the most recent action.
 75        action = state.get_previous_action()
 76        if (action is not None):
 77            self._output_stream.write(f"Previous action: '{action}'.\n")
 78
 79        if (termination_reason is not None):
 80            self._output_stream.write('Game Over!\n')
 81            self._output_stream.write(f"Termination Reason: '{termination_reason}'.\n")
 82
 83        self._output_stream.write('\n')
 84        self._output_stream.flush()
 85
 86    def _get_padding(self) -> str:
 87        """ Get the padding for this UI. """
 88
 89        return " " * self._padding_size
 90
 91    def _get_prefix_padding(self, state: chessai.core.gamestate.GameState) -> str:
 92        """
 93        Get the initial padding for rows without a column numbering,
 94        such as the horizontal border and the file labels.
 95        """
 96
 97        prefix_padding_size = self._get_prefix_padding_size(state)
 98
 99        prefix_padding = ' ' * prefix_padding_size
100
101        return f"{prefix_padding}{self._get_padding()}"
102
103
104    def _get_prefix_padding_size(self, state: chessai.core.gamestate.GameState) -> int:
105        """ Get the size of the prefix padding to account for the rank labels. """
106
107        prefix_padding_size = 1
108        num_ranks = state.board.num_ranks
109
110        while (num_ranks >= 10):
111            prefix_padding_size += 1
112            num_ranks = num_ranks // 10
113
114        return prefix_padding_size
115
116    def _get_horizontal_border(self, state: chessai.core.gamestate.GameState) -> str:
117        """ Get the horizontal border for the UI. """
118
119        if (self._horizontal_border is not None):
120            return self._horizontal_border
121
122        dash_segment = '-' * self._cell_width + '+'
123
124        horizontal_border = f"{self._get_prefix_padding(state)}+" + (dash_segment * state.board.num_files) + '\n'
125
126        # Cache the horizontal border for subsequent calls.
127        self._horizontal_border = horizontal_border
128
129        return self._horizontal_border
130
131    def _get_file_labels(self, state: chessai.core.gamestate.GameState) -> str:
132        """ Get the labels of the files for the bottom of the board. """
133
134        if (self._file_labels is not None):
135            return self._file_labels
136
137        file_labels = self._get_prefix_padding(state)
138        for file in range(state.board.num_files):
139            # Use a coordinate to get the file name.
140            coordinate = chessai.core.coordinate.Coordinate(file, 0)
141
142            file_str = coordinate.uci(only_file = True)
143            file_labels += f"{self._get_padding()}{file_str}{self._get_padding()}"
144
145        file_labels += '\n'
146
147        # Cache the file labels for subsequent calls to the UI.
148        self._file_labels = file_labels
149
150        return file_labels
151
152    def _translate_piece(self, piece: chessai.core.piece.Piece | None) -> str:
153        """
154        Convert a piece to a string.
155        This can be trivial (since a piece knows how to represent itself as a string),
156        but this allows children to implement special conversions.
157        """
158
159        if (piece is None):
160            return ' ' * self.SYMBOL_SIZE
161
162        return piece.unicode_symbol().center(self.SYMBOL_SIZE)

A simple UI that renders the game to a text stream. This UI will be simple and generally meant for debugging.

TextUI( output_stream: <class 'TextIO'>, padding_size: int = 1, **kwargs: Any)
24    def __init__(self,
25            output_stream: typing.TextIO,
26            padding_size: int = DEFAULT_PADDING_SIZE,
27            **kwargs: typing.Any) -> None:
28        super().__init__(**kwargs)
29
30        self._output_stream: typing.TextIO = output_stream
31        """ The stream output will be sent to. """
32
33        self._padding_size: int = padding_size
34        """ The number of padding spaces between each piece and the square border. """
35
36        self._cell_width: int = self.SYMBOL_SIZE
37        """ The width of an individual cell, including the padding and symbol sizes. """
38
39        self._file_labels: str | None = None
40        """ The file labels to be displayed at the bottom of the board. """
41
42        self._horizontal_border: str | None = None
43        """ The horizontal border to be displayed between each rank. """
SYMBOL_SIZE: int = 2

The space reserved to display the symbols. Pieces represent themselves as unicode symbols, which often take two spaces.

def draw( self, state: chessai.core.gamestate.GameState, termination_reason: chessai.core.types.TerminationReason | None = None, **kwargs: Any) -> None:
45    def draw(self,
46            state: chessai.core.gamestate.GameState,
47            termination_reason: chessai.core.types.TerminationReason | None = None,
48            **kwargs: typing.Any,
49            ) -> None:
50        if (state.get_previous_action() == chessai.core.action.NoneAction()):
51            return
52
53        # Draw the top border.
54        self._output_stream.write(self._get_horizontal_border(state))
55
56        prefix_padding_size = self._get_prefix_padding_size(state)
57
58        for rank in range((state.board.num_ranks - 1), -1, -1):
59            rank_str = str(rank + 1)
60            rank_padding_size = prefix_padding_size - len(rank_str)
61            rank_padding = ' ' * rank_padding_size
62
63            line = f"{rank_str}{rank_padding}{self._get_padding()}|"
64
65            for file in range(state.board.num_files):
66                piece_symbol = self._translate_piece(state.board.get(file, rank))
67                line += f"{piece_symbol}|"
68
69            self._output_stream.write(line + "\n")
70            self._output_stream.write(self._get_horizontal_border(state))
71
72        self._output_stream.write(self._get_file_labels(state))
73
74        # Display the most recent action.
75        action = state.get_previous_action()
76        if (action is not None):
77            self._output_stream.write(f"Previous action: '{action}'.\n")
78
79        if (termination_reason is not None):
80            self._output_stream.write('Game Over!\n')
81            self._output_stream.write(f"Termination Reason: '{termination_reason}'.\n")
82
83        self._output_stream.write('\n')
84        self._output_stream.flush()

Visualize the state of the game to the UI. This is the typically the main override point for children. Note that how this method visualizes the game completely unrelated to how the draw_image() method works. draw() will render to whatever the specific UI for the child class is, while draw_image() specifically creates an image which will be used for animations. If the child UI is also image-based than it can leverage draw_image(), but there is no requirement to do that.

class StdioUI(TextUI):
164class StdioUI(TextUI):
165    """
166    A convenience class for a TextUI using stdout.
167    """
168
169    def __init__(self, **kwargs: typing.Any) -> None:
170        super().__init__(sys.stdout, **kwargs)

A convenience class for a TextUI using stdout.

StdioUI(**kwargs: Any)
169    def __init__(self, **kwargs: typing.Any) -> None:
170        super().__init__(sys.stdout, **kwargs)