diff --git a/docs/_static/components/holder.png b/docs/_static/components/holder.png new file mode 100644 index 0000000..8bf7588 Binary files /dev/null and b/docs/_static/components/holder.png differ diff --git a/docs/_static/components/queue.png b/docs/_static/components/queue.png new file mode 100644 index 0000000..8bf7588 Binary files /dev/null and b/docs/_static/components/queue.png differ diff --git a/docs/_static/components/tetromino.png b/docs/_static/components/tetromino.png new file mode 100644 index 0000000..8bf7588 Binary files /dev/null and b/docs/_static/components/tetromino.png differ diff --git a/docs/components/holder.md b/docs/components/holder.md new file mode 100644 index 0000000..5a50537 --- /dev/null +++ b/docs/components/holder.md @@ -0,0 +1,14 @@ +# Holder + +![Queue](../_static/components/holder.png) + +```{eval-rst} +.. autoclass:: tetris_gymnasium.components.tetromino_holder.TetrominoHolder +``` + +## Methods +```{eval-rst} +.. automethod:: tetris_gymnasium.components.tetromino_holder.TetrominoHolder.swap +.. automethod:: tetris_gymnasium.components.tetromino_holder.TetrominoHolder.reset +.. automethod:: tetris_gymnasium.components.tetromino_holder.TetrominoHolder.get_tetrominoes +``` diff --git a/docs/components/queue.md b/docs/components/queue.md new file mode 100644 index 0000000..87aa633 --- /dev/null +++ b/docs/components/queue.md @@ -0,0 +1,14 @@ +# Queue + +![Queue](../_static/components/queue.png) + +```{eval-rst} +.. autoclass:: tetris_gymnasium.components.tetromino_queue.TetrominoQueue +``` + +## Methods +```{eval-rst} +.. automethod:: tetris_gymnasium.components.tetromino_queue.TetrominoQueue.reset +.. automethod:: tetris_gymnasium.components.tetromino_queue.TetrominoQueue.get_next_tetromino +.. automethod:: tetris_gymnasium.components.tetromino_queue.TetrominoQueue.get_queue +``` diff --git a/docs/components/randomizer.md b/docs/components/randomizer.md new file mode 100644 index 0000000..d8294ab --- /dev/null +++ b/docs/components/randomizer.md @@ -0,0 +1,35 @@ +# Randomizer + +```{eval-rst} +.. autoclass:: tetris_gymnasium.components.tetromino_randomizer.Randomizer +``` + +## Methods +```{eval-rst} +.. automethod:: tetris_gymnasium.components.tetromino_randomizer.Randomizer.get_next_tetromino +.. automethod:: tetris_gymnasium.components.tetromino_randomizer.Randomizer.reset +``` + +## Implementations + +In Tetris Gymnasium, there are different randomizers available by default. The default randomizer is the `BagRandomizer`, +which is the same as the one used in the most Tetris games. The `TrueRandomizer` is a randomizer that generates +tetrominoes with a uniform distribution. + +If these randomizers do not fit your needs, you can easily implement your own randomizer by subclassing the `Randomizer`. + +```{eval-rst} +.. autoclass:: tetris_gymnasium.components.tetromino_randomizer.BagRandomizer +``` + +```{eval-rst} +.. automethod:: tetris_gymnasium.components.tetromino_randomizer.BagRandomizer.get_next_tetromino +``` + +```{eval-rst} +.. autoclass:: tetris_gymnasium.components.tetromino_randomizer.TrueRandomizer +``` + +```{eval-rst} +.. automethod:: tetris_gymnasium.components.tetromino_randomizer.TrueRandomizer.get_next_tetromino +``` diff --git a/docs/components/tetromino.md b/docs/components/tetromino.md new file mode 100644 index 0000000..e3cb87d --- /dev/null +++ b/docs/components/tetromino.md @@ -0,0 +1,8 @@ +# Tetromino + +![Tetromino](../_static/components/tetromino.png) + +```{eval-rst} +.. autoclass:: tetris_gymnasium.components.tetromino.Pixel +.. autoclass:: tetris_gymnasium.components.tetromino.Tetromino +``` diff --git a/docs/conf.py b/docs/conf.py index 0095808..180c202 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,5 +46,25 @@ "source_repository": "https://github.com/Max-We/Tetris-Gymnasium", "source_branch": "main", "source_directory": "docs/", - "announcement": "Tetris Gymnasium is under early development!", + # "announcement": "Tetris Gymnasium is under early development!", } + +# Autodoc +autoclass_content = "both" +autodoc_preserve_defaults = True + + +# This function removes the content before the parameters in the __init__ function. +# This content is often not useful for the website documentation as it replicates +# the class docstring. +def remove_lines_before_parameters(app, what, name, obj, options, lines): + if what == "class": + # ":param" represents args values + first_idx_to_keep = next( + (i for i, line in enumerate(lines) if line.startswith(":param")), 0 + ) + lines[:] = lines[first_idx_to_keep:] + + +def setup(app): + app.connect("autodoc-process-docstring", remove_lines_before_parameters) diff --git a/docs/index.md b/docs/index.md index 0876f7b..4bbdcfb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -45,12 +45,23 @@ introduction/quickstart ```{toctree} :maxdepth: 2 -:caption: API +:caption: Environment :hidden: environments/tetris ``` +```{toctree} +:maxdepth: 2 +:caption: Components +:hidden: + +components/tetromino +components/queue +components/holder +components/randomizer +``` + ```{toctree} :maxdepth: 2 :caption: Development diff --git a/examples/play_interactive.py b/examples/play_interactive.py index a07a187..7af6769 100644 --- a/examples/play_interactive.py +++ b/examples/play_interactive.py @@ -11,8 +11,9 @@ tetris_game = gym.make("tetris_gymnasium/Tetris", render_mode="rgb_array") tetris_game.reset(seed=42) - cv2.namedWindow("Tetris", cv2.WINDOW_GUI_NORMAL) - cv2.resizeWindow("Tetris", 200, 400) + window_name = "Tetris Gymnasium" + cv2.namedWindow(window_name, cv2.WINDOW_GUI_NORMAL) + cv2.resizeWindow(window_name, 200, 400) # Main game loop terminated = False @@ -22,7 +23,7 @@ # Render the current state of the game as an image using CV2q # CV2 uses BGR color format, so we need to convert the RGB image to BGR - cv2.imshow("Tetris", cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)) + cv2.imshow(window_name, cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)) cv2.waitKey(50) # Pick an action from user input mapped to the keyboard @@ -48,7 +49,7 @@ tetris_game.reset(seed=42) break - if cv2.getWindowProperty("Tetris", cv2.WND_PROP_VISIBLE) == 0: + if cv2.getWindowProperty(window_name, cv2.WND_PROP_VISIBLE) == 0: sys.exit() # Perform the action diff --git a/examples/play_interactive_cnn.py b/examples/play_interactive_cnn.py index 96b2ace..b6aa237 100644 --- a/examples/play_interactive_cnn.py +++ b/examples/play_interactive_cnn.py @@ -12,8 +12,9 @@ tetris_game.reset(seed=42) tetris_game = CnnObservation(tetris_game) - cv2.namedWindow("Tetris", cv2.WINDOW_GUI_NORMAL) - cv2.resizeWindow("Tetris", 400, 250) + window_name = "Tetris Gymnasium" + cv2.namedWindow(window_name, cv2.WINDOW_GUI_NORMAL) + cv2.resizeWindow(window_name, 395, 250) # Main game loop terminated = False @@ -23,7 +24,7 @@ # Render the current state of the game as an image using CV2 # CV2 uses BGR color format, so we need to convert the RGB image to BGR - cv2.imshow("Tetris", cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)) + cv2.imshow(window_name, cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)) cv2.waitKey(50) # Pick an action from user input mapped to the keyboard @@ -49,7 +50,7 @@ tetris_game.reset(seed=42) break - if cv2.getWindowProperty("Tetris", cv2.WND_PROP_VISIBLE) == 0: + if cv2.getWindowProperty(window_name, cv2.WND_PROP_VISIBLE) == 0: sys.exit() # Perform the action diff --git a/tetris_gymnasium/components/tetromino.py b/tetris_gymnasium/components/tetromino.py index c24962c..606fcc0 100644 --- a/tetris_gymnasium/components/tetromino.py +++ b/tetris_gymnasium/components/tetromino.py @@ -6,9 +6,12 @@ @dataclass class Pixel: - """A single pixel of a Tetris game. + """A single pixel in a game of Tetris. - A pixel can be part of a tetromino or part of the game board (empty, bedrock). + A pixel is the basic building block of the game and has an id and a color. + + The basic pixels are in the most cases the empty pixel (id=0) and the bedrock pixel (id=1). + Additionally, multiple pixels can be combined to form a tetromino. """ id: int @@ -17,9 +20,25 @@ class Pixel: @dataclass class Tetromino(Pixel): - """A Tetris piece. + """A Tetris "piece" is called a Tetromino. Examples are the I, J, L, O, S, T, and Z pieces. + + On a conceptual basis, a tetromino is a 2D-array composed of multiple pixels. All pixels that compose the tetromino + have the same id. And the ids of all the pixels are stored in the matrix. + + An example for the matrix of the T-tetromino: + + .. code-block:: python + + [ + [0, 1, 0], + [1, 1, 1], + [0, 0, 0] + ] + + In the matrix, the value `0` represents an empty pixel, and the value `1` represents a pixel of the T-tetromino. - A tetromino is a geometric shape composed of multiple pixels. + When initializing a `Tetromino` object on your own, you'll typically use binary values for the matrix, where `1` + represents a pixel of the tetromino and `0` represents an empty pixel. """ matrix: np.ndarray diff --git a/tetris_gymnasium/components/tetromino_holder.py b/tetris_gymnasium/components/tetromino_holder.py index ff4419b..905f2f4 100644 --- a/tetris_gymnasium/components/tetromino_holder.py +++ b/tetris_gymnasium/components/tetromino_holder.py @@ -6,7 +6,10 @@ class TetrominoHolder: - """Class for one or more tetrominoes for later use in a game of Tetris.""" + """A holder can store one or more tetrominoes for later use in a game of Tetris. + + Tetrominoes can be swapped in- and out during the game. + """ def __init__(self, size=1): """Create a new holder with the given number of tetrominoes. @@ -28,13 +31,14 @@ def _store_tetromino(self, tetromino: Tetromino): def swap(self, tetromino: Tetromino) -> Optional[Tetromino]: """Swap the given tetromino with the one in the holder. - This implementation uses a queue to store the tetrominoes. Tetromioes are only returned once the queue is full. + This implementation uses a queue to store the tetrominoes. Tetrominoes are only returned once the queue is full. + If this is not the case, the provided tetromino is stored in the queue and None is returned. Args: tetromino: The tetromino to store in the holder. Returns: - The tetromino that was in the holder before the swap. + The oldest tetromino that's stored in the queue, if the queue is full. Otherwise, None. """ if len(self.queue) < self.size: self._store_tetromino(tetromino) @@ -45,9 +49,9 @@ def swap(self, tetromino: Tetromino) -> Optional[Tetromino]: return result def reset(self): - """Reset the holder to its initial state.""" + """Reset the holder to its initial state. This involves clearing the queue.""" self.queue.clear() def get_tetrominoes(self): - """Get the tetrominoes currently in the holder.""" + """Get all the tetrominoes currently in the holder.""" return list(self.queue) diff --git a/tetris_gymnasium/components/tetromino_queue.py b/tetris_gymnasium/components/tetromino_queue.py index 9bb98d9..6f458c6 100644 --- a/tetris_gymnasium/components/tetromino_queue.py +++ b/tetris_gymnasium/components/tetromino_queue.py @@ -5,7 +5,10 @@ class TetrominoQueue: - """Class for a queue of tetrominoes for use in a game of Tetris.""" + """The queue shows the incoming tetrominoes in a game of Tetris. + + The sequence of pieces is generated by a :class:`Randomizer`, which can be customized by the user. + """ def __init__(self, randomizer: Randomizer, size=4): """Create a new queue of tetrominoes with the given size. @@ -19,18 +22,25 @@ def __init__(self, randomizer: Randomizer, size=4): self.size = size def reset(self, seed=None): - """Reset the queue to its initial state.""" + """Reset the queue to its initial state. + + Args: + seed: The seed to use for the randomizer. Defaults to None. + """ self.randomizer.reset(seed) self.queue.clear() for _ in range(self.size): self.queue.append(self.randomizer.get_next_tetromino()) def get_next_tetromino(self): - """Get the next tetromino from the queue and generates a new one.""" + """Gets the next tetromino from the queue and generates a new one. + + Generating a new Tetromino makes sure that the queue will always be full. + """ tetromino = self.queue.popleft() self.queue.append(self.randomizer.get_next_tetromino()) return tetromino def get_queue(self): - """Get the tetrominoes currently in the queue.""" + """Get all tetrominoes currently in the queue.""" return list(self.queue) diff --git a/tetris_gymnasium/components/tetromino_randomizer.py b/tetris_gymnasium/components/tetromino_randomizer.py index ddbc453..87d2860 100644 --- a/tetris_gymnasium/components/tetromino_randomizer.py +++ b/tetris_gymnasium/components/tetromino_randomizer.py @@ -6,12 +6,15 @@ class Randomizer: - """Abstract class for tetromino randomizers.""" + """Abstract class for tetromino randomizers. - def __init__(self, size: int): - """Create a new randomizer with the given number of tetrominoes. + A randomizer is an object that can be used to generate the order of tetrominoes in a game of Tetris. When it's + called via :func:`get_next_tetromino`, it returns the **index** of the next tetromino to be used in the game. + This information can be used by the caller to get the actual tetromino object from a list of tetrominoes. + """ - A randomizer is an object that can be used to generate the order of tetrominoes in a game of Tetris. + def __init__(self, size: int): + """Create a randomizer for a specified number of tetrominoes to choose from. Args: size: The number of tetrominoes to choose from. @@ -29,7 +32,7 @@ def get_next_tetromino(self) -> int: @abstractmethod def reset(self, seed=None): - """Resets the randomizer to start from a fresh state. + """Resets the randomizer. This function is implemented after the usage pattern in Gymnasium, where seed is passed to the reset function only for the very first call after initialization. In all other cases, seed=None and the RNG is not reset. @@ -46,19 +49,25 @@ def reset(self, seed=None): class BagRandomizer(Randomizer): """Randomly selects tetrominoes from a bag, ensuring that each tetromino is used once before reshuffling. + The bag randomizer is a common and popular approach in Tetris. It ensures that each tetromino is used once before + reshuffling the bag, thus avoiding long sequences of the same tetromino. The functionality is explained on the tetris wiki page: https://tetris.fandom.com/wiki/Random_Generator """ def __init__(self, size): - """Create a new bag randomizer with the given number of tetrominoes.""" + """Create a new bag randomizer for a specified number of tetrominoes to choose from. + + Args: + size: The number of tetrominoes to choose from. + """ super().__init__(size) self.bag = np.arange(self.size, dtype=np.int8) self.index = 0 def get_next_tetromino(self) -> int: - """The bag randomizer returns the next tetromino in the bag. + """Samples a new tetromino from the bag. - If the end of the bag is reached, the bag is reshuffled and the process starts over. + Once the bag has been fully exploited, it is reshuffled and the process starts over. Returns: The index of the next tetromino to be used in the game. """ @@ -71,7 +80,7 @@ def get_next_tetromino(self) -> int: return tetromino_index def shuffle_bag(self): - """Shuffle the bag and reset the index to restart.""" + """Shuffle the bag and reset the index to restart the sampling process.""" self.rng.shuffle(self.bag) self.index = 0 # Reset index to the start @@ -82,10 +91,14 @@ def reset(self, seed=None): class TrueRandomizer(Randomizer): - """Randomly selects tetrominoes.""" + """Randomly selects tetrominoes. + + This is the simplest form of randomizer, where each tetromino is chosen randomly. This approach can lead to + sequences of the same tetromino, which may or may not be desired. + """ def __init__(self, size): - """Create a new random scheduler with the given number of tetrominoes. + """Create a new true randomizer for a specified number of tetrominoes to choose from. Args: size: The number of tetrominoes to choose from. @@ -93,10 +106,11 @@ def __init__(self, size): super().__init__(size) def get_next_tetromino(self) -> int: - """Return a random tetromino index.""" + """Samples a new tetromino randomly.""" return self.rng.randint(0, self.size) def reset(self, seed=None): """Resets the randomizer to start from a fresh state.""" # In the case of `TrueRandomizer`, there is no state to reset + # In this case, only the RNG is reset with the specified seed super().reset(seed)