pacai.ui.text

  1import atexit
  2import queue
  3import sys
  4import termios  # pylint: disable=import-error
  5import threading
  6import typing
  7
  8import pacai.core.action
  9import pacai.core.board
 10import pacai.core.gamestate
 11import pacai.core.ui
 12
 13class TextStreamUserInputDevice(pacai.core.ui.UserInputDevice):
 14    """
 15    A user input device that watches a text stream for input.
 16    The text stream could be a wide range of things,
 17    including a file or stdin.
 18    The target text stream will be closed when this device is closed.
 19    If the target stream is a tty (e.g., stdin),
 20    then the tty's attributes will be adjusted for streaming input.
 21    """
 22
 23    def __init__(self,
 24            input_stream: typing.TextIO,
 25            char_mapping: dict[str, pacai.core.action.Action] | None = None,
 26            **kwargs: typing.Any) -> None:
 27        self._input_stream: typing.TextIO = input_stream
 28        """ Where to get input from. """
 29
 30        if (char_mapping is None):
 31            char_mapping = pacai.core.ui.DUAL_CHAR_MAPPING
 32
 33        self._char_mapping: dict[str, pacai.core.action.Action] = char_mapping
 34        """ Map characters to actions. """
 35
 36        self._chars_queue: queue.Queue = queue.Queue()
 37        """ Used to store characters coming from the input stream. """
 38
 39        self._thread: threading.Thread = threading.Thread(target = _watch_text_stream, args = (self._input_stream, self._chars_queue))
 40        """ The thread that does the actual reading. """
 41
 42        self._old_settings: list | None = None
 43        """ Keep track of the old tty settings so we can reset properly. """
 44
 45        self._set_tty_attributes()
 46
 47        self._thread.start()
 48
 49    def get_inputs(self) -> list[pacai.core.action.Action]:
 50        output: list[pacai.core.action.Action] = []
 51        while (not self._chars_queue.empty()):
 52            char = self._chars_queue.get(block = False)
 53            if (char in self._char_mapping):
 54                output.append(self._char_mapping[char])
 55
 56        return output
 57
 58    def close(self) -> None:
 59        self._reset_tty_attributes()
 60        self._input_stream.close()
 61        self._thread.join()
 62
 63        super().close()
 64
 65    def __dict__(self) -> dict[str, typing.Any]:  # type: ignore[override]
 66        """ Do not allow for serialization because of the threads and streams. """
 67
 68        raise ValueError(f"This class ('{type(self).__qualname__}') cannot be serialized.")
 69
 70    def _set_tty_attributes(self) -> None:
 71        """ If the target stream is a tty, then set properties for better streaming input. """
 72
 73        # If we are not a tty, then there is nothing special to do.
 74        if (not self._input_stream.isatty()):
 75            return
 76
 77        # Do a platform check for POSIX.
 78        if (sys.platform.startswith("win")):
 79            raise ValueError("Terminal (tty) user input devices are not supported on Windows.")
 80
 81        self._old_settings = termios.tcgetattr(self._input_stream)
 82
 83        # Since the behavior of the terminal can be changed by this class,
 84        # ensure everything is reset when the program exits.
 85        atexit.register(self._reset_tty_attributes)
 86
 87        new_settings = termios.tcgetattr(self._input_stream)
 88
 89        # Modify lflags.
 90        # Remove echo and canonical (line-by-line) mode.
 91        new_settings[3] = new_settings[3] & ~(termios.ECHO | termios.ICANON)
 92
 93        # Modify CC flags.
 94        # Set non-canonical mode min chars and timeout.
 95        new_settings[6][termios.VMIN] = 1
 96        new_settings[6][termios.VTIME] = 0
 97
 98        termios.tcsetattr(self._input_stream, termios.TCSAFLUSH, new_settings)
 99
100    def _reset_tty_attributes(self) -> None:
101        if (self._old_settings is not None):
102            # Note that Windows does not have these settings.
103            if (hasattr(termios, 'tcsetattr') and hasattr(termios, 'TCSADRAIN')):
104                termios.tcsetattr(self._input_stream, termios.TCSADRAIN, self._old_settings)  # type: ignore[attr-defined,unused-ignore]
105
106            self._old_settings = None
107
108def _watch_text_stream(input_stream: typing.TextIO, result_queue: queue.Queue) -> None:
109    """ A thread worker to watch a text stream and relay the input. """
110
111    while (True):
112        try:
113            next_char = input_stream.read(1)
114        except ValueError:
115            # Should indicate that the stream was closed.
116            return
117
118        # Check for an EOF.
119        if ((next_char is None) or (len(next_char) == 0)):
120            return
121
122        result_queue.put(next_char, block = False)
123
124class TextUI(pacai.core.ui.UI):
125    """
126    A simple UI that renders the game to a text stream and takes input from another text stream.
127    This UI will be simple and generally meant for debugging.
128    """
129
130    def __init__(self,
131            input_stream: typing.TextIO,
132            output_stream: typing.TextIO,
133            **kwargs: typing.Any) -> None:
134        input_device = TextStreamUserInputDevice(input_stream, **kwargs)
135        super().__init__(user_input_device = input_device, **kwargs)
136
137        self._output_stream: typing.TextIO = output_stream
138        """ The stream output will be sent to. """
139
140    def draw(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> None:
141        grid = state.board.to_grid()
142        for row in grid:
143            line = ''.join([self._translate_marker(marker, state) for marker in row])
144            self._output_stream.write(line + "\n")
145
146        self._output_stream.write(f"Score: {state.score}\n")
147
148        if (state.game_over):
149            self._output_stream.write('Game Over!\n')
150
151        self._output_stream.write("\n")
152        self._output_stream.flush()
153
154    def _translate_marker(self, marker: pacai.core.board.Marker, state: pacai.core.gamestate.GameState) -> str:
155        """
156        Convert a marker to a string.
157        This is generally trivial (since a marker is already a string),
158        but this allows children to implement special conversions.
159        """
160
161        return marker
162
163class StdioUI(TextUI):
164    """
165    A convenience class for a TextUI using stdin and stdout.
166    """
167
168    def __init__(self, **kwargs: typing.Any) -> None:
169        super().__init__(sys.stdin, sys.stdout, **kwargs)
class TextStreamUserInputDevice(pacai.core.ui.UserInputDevice):
 14class TextStreamUserInputDevice(pacai.core.ui.UserInputDevice):
 15    """
 16    A user input device that watches a text stream for input.
 17    The text stream could be a wide range of things,
 18    including a file or stdin.
 19    The target text stream will be closed when this device is closed.
 20    If the target stream is a tty (e.g., stdin),
 21    then the tty's attributes will be adjusted for streaming input.
 22    """
 23
 24    def __init__(self,
 25            input_stream: typing.TextIO,
 26            char_mapping: dict[str, pacai.core.action.Action] | None = None,
 27            **kwargs: typing.Any) -> None:
 28        self._input_stream: typing.TextIO = input_stream
 29        """ Where to get input from. """
 30
 31        if (char_mapping is None):
 32            char_mapping = pacai.core.ui.DUAL_CHAR_MAPPING
 33
 34        self._char_mapping: dict[str, pacai.core.action.Action] = char_mapping
 35        """ Map characters to actions. """
 36
 37        self._chars_queue: queue.Queue = queue.Queue()
 38        """ Used to store characters coming from the input stream. """
 39
 40        self._thread: threading.Thread = threading.Thread(target = _watch_text_stream, args = (self._input_stream, self._chars_queue))
 41        """ The thread that does the actual reading. """
 42
 43        self._old_settings: list | None = None
 44        """ Keep track of the old tty settings so we can reset properly. """
 45
 46        self._set_tty_attributes()
 47
 48        self._thread.start()
 49
 50    def get_inputs(self) -> list[pacai.core.action.Action]:
 51        output: list[pacai.core.action.Action] = []
 52        while (not self._chars_queue.empty()):
 53            char = self._chars_queue.get(block = False)
 54            if (char in self._char_mapping):
 55                output.append(self._char_mapping[char])
 56
 57        return output
 58
 59    def close(self) -> None:
 60        self._reset_tty_attributes()
 61        self._input_stream.close()
 62        self._thread.join()
 63
 64        super().close()
 65
 66    def __dict__(self) -> dict[str, typing.Any]:  # type: ignore[override]
 67        """ Do not allow for serialization because of the threads and streams. """
 68
 69        raise ValueError(f"This class ('{type(self).__qualname__}') cannot be serialized.")
 70
 71    def _set_tty_attributes(self) -> None:
 72        """ If the target stream is a tty, then set properties for better streaming input. """
 73
 74        # If we are not a tty, then there is nothing special to do.
 75        if (not self._input_stream.isatty()):
 76            return
 77
 78        # Do a platform check for POSIX.
 79        if (sys.platform.startswith("win")):
 80            raise ValueError("Terminal (tty) user input devices are not supported on Windows.")
 81
 82        self._old_settings = termios.tcgetattr(self._input_stream)
 83
 84        # Since the behavior of the terminal can be changed by this class,
 85        # ensure everything is reset when the program exits.
 86        atexit.register(self._reset_tty_attributes)
 87
 88        new_settings = termios.tcgetattr(self._input_stream)
 89
 90        # Modify lflags.
 91        # Remove echo and canonical (line-by-line) mode.
 92        new_settings[3] = new_settings[3] & ~(termios.ECHO | termios.ICANON)
 93
 94        # Modify CC flags.
 95        # Set non-canonical mode min chars and timeout.
 96        new_settings[6][termios.VMIN] = 1
 97        new_settings[6][termios.VTIME] = 0
 98
 99        termios.tcsetattr(self._input_stream, termios.TCSAFLUSH, new_settings)
100
101    def _reset_tty_attributes(self) -> None:
102        if (self._old_settings is not None):
103            # Note that Windows does not have these settings.
104            if (hasattr(termios, 'tcsetattr') and hasattr(termios, 'TCSADRAIN')):
105                termios.tcsetattr(self._input_stream, termios.TCSADRAIN, self._old_settings)  # type: ignore[attr-defined,unused-ignore]
106
107            self._old_settings = None

A user input device that watches a text stream for input. The text stream could be a wide range of things, including a file or stdin. The target text stream will be closed when this device is closed. If the target stream is a tty (e.g., stdin), then the tty's attributes will be adjusted for streaming input.

TextStreamUserInputDevice( input_stream: <class 'TextIO'>, char_mapping: dict[str, pacai.core.action.Action] | None = None, **kwargs: Any)
24    def __init__(self,
25            input_stream: typing.TextIO,
26            char_mapping: dict[str, pacai.core.action.Action] | None = None,
27            **kwargs: typing.Any) -> None:
28        self._input_stream: typing.TextIO = input_stream
29        """ Where to get input from. """
30
31        if (char_mapping is None):
32            char_mapping = pacai.core.ui.DUAL_CHAR_MAPPING
33
34        self._char_mapping: dict[str, pacai.core.action.Action] = char_mapping
35        """ Map characters to actions. """
36
37        self._chars_queue: queue.Queue = queue.Queue()
38        """ Used to store characters coming from the input stream. """
39
40        self._thread: threading.Thread = threading.Thread(target = _watch_text_stream, args = (self._input_stream, self._chars_queue))
41        """ The thread that does the actual reading. """
42
43        self._old_settings: list | None = None
44        """ Keep track of the old tty settings so we can reset properly. """
45
46        self._set_tty_attributes()
47
48        self._thread.start()
def get_inputs(self) -> list[pacai.core.action.Action]:
50    def get_inputs(self) -> list[pacai.core.action.Action]:
51        output: list[pacai.core.action.Action] = []
52        while (not self._chars_queue.empty()):
53            char = self._chars_queue.get(block = False)
54            if (char in self._char_mapping):
55                output.append(self._char_mapping[char])
56
57        return output

Get any inputs that have occurred since the last call to this method. This method is responsible for not returning the same input instance in subsequent calls. The last input in the returned list should be the most recent input.

def close(self) -> None:
59    def close(self) -> None:
60        self._reset_tty_attributes()
61        self._input_stream.close()
62        self._thread.join()
63
64        super().close()

Close the user input channel and release all owned resources.

class TextUI(pacai.core.ui.UI):
125class TextUI(pacai.core.ui.UI):
126    """
127    A simple UI that renders the game to a text stream and takes input from another text stream.
128    This UI will be simple and generally meant for debugging.
129    """
130
131    def __init__(self,
132            input_stream: typing.TextIO,
133            output_stream: typing.TextIO,
134            **kwargs: typing.Any) -> None:
135        input_device = TextStreamUserInputDevice(input_stream, **kwargs)
136        super().__init__(user_input_device = input_device, **kwargs)
137
138        self._output_stream: typing.TextIO = output_stream
139        """ The stream output will be sent to. """
140
141    def draw(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> None:
142        grid = state.board.to_grid()
143        for row in grid:
144            line = ''.join([self._translate_marker(marker, state) for marker in row])
145            self._output_stream.write(line + "\n")
146
147        self._output_stream.write(f"Score: {state.score}\n")
148
149        if (state.game_over):
150            self._output_stream.write('Game Over!\n')
151
152        self._output_stream.write("\n")
153        self._output_stream.flush()
154
155    def _translate_marker(self, marker: pacai.core.board.Marker, state: pacai.core.gamestate.GameState) -> str:
156        """
157        Convert a marker to a string.
158        This is generally trivial (since a marker is already a string),
159        but this allows children to implement special conversions.
160        """
161
162        return marker

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

TextUI( input_stream: <class 'TextIO'>, output_stream: <class 'TextIO'>, **kwargs: Any)
131    def __init__(self,
132            input_stream: typing.TextIO,
133            output_stream: typing.TextIO,
134            **kwargs: typing.Any) -> None:
135        input_device = TextStreamUserInputDevice(input_stream, **kwargs)
136        super().__init__(user_input_device = input_device, **kwargs)
137
138        self._output_stream: typing.TextIO = output_stream
139        """ The stream output will be sent to. """
def draw(self, state: pacai.core.gamestate.GameState, **kwargs: Any) -> None:
141    def draw(self, state: pacai.core.gamestate.GameState, **kwargs: typing.Any) -> None:
142        grid = state.board.to_grid()
143        for row in grid:
144            line = ''.join([self._translate_marker(marker, state) for marker in row])
145            self._output_stream.write(line + "\n")
146
147        self._output_stream.write(f"Score: {state.score}\n")
148
149        if (state.game_over):
150            self._output_stream.write('Game Over!\n')
151
152        self._output_stream.write("\n")
153        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 stdin and stdout.
167    """
168
169    def __init__(self, **kwargs: typing.Any) -> None:
170        super().__init__(sys.stdin, sys.stdout, **kwargs)

A convenience class for a TextUI using stdin and stdout.

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