Grid - Snailz Docs

Skip to content

# Grid

Grid generation.

To create a grid using invasion percolation:

1. Generate an NxN grid of random numbers.
2. Mark the center cell as "filled" by negating its value.
3. On each iteration:
   1. Find the lowest-valued cell adjacent to the filled region.
   2. Fill that in by negating its value.
   3. If several cells tie for lowest value, pick one at random.
4. Stop when the filled region hits the edge of the grid.

Instead of repeatedly searching for cells adjacent to the filled region, the grid keeps a value-to-coordinates dictionary. When a cell is filled in, neighbors not already recorded are added.

The grid is saved as a list of lists. 0 shows unfilled cells, while positive numbers show the original value of filled cells.

## `GridParams`

Bases: `BaseModel`

Parameters for grid generation.

This class validates the parameters used for grid generation with the following fields:

- depth: The maximum value for grid cells (must be positive)
- seed: Random seed for reproducibility
- size: Grid size (size × size, must be positive)

Source code in `src/snailz/grid.py`

    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    class GridParams(BaseModel):
        """Parameters for grid generation.

        This class validates the parameters used for grid generation with the following fields:

        - depth: The maximum value for grid cells (must be positive)
        - seed: Random seed for reproducibility
        - size: Grid size (size × size, must be positive)
        """

        depth: int = Field(gt=0, description="The maximum value for grid cells")
        seed: int = Field(description="Random seed for reproducibility")
        size: int = Field(gt=0, description="Grid size (size × size)")

        model_config = {"extra": "forbid"}

## `Grid`

Bases: `BaseModel`

Keep track of generated grid.

Source code in `src/snailz/grid.py`

    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    class Grid(BaseModel):
        """Keep track of generated grid."""

        grid: list[list[int]]
        params: GridParams

        def to_csv(self) -> str:
            """Return a CSV string representation of the grid data.

            Returns:
                A CSV-formatted string containing the grid values without a header row,
                using Unix line endings (LF).
            """
            output = io.StringIO(newline="\n")
            writer = csv.writer(output, lineterminator="\n")
            for row in self.grid:
                writer.writerow(row)

            return output.getvalue()

### `to_csv()`

Return a CSV string representation of the grid data.

Returns:

Type | Description
--- | ---
`str` | A CSV-formatted string containing the grid values without a header row,
`str` | using Unix line endings (LF).

Source code in `src/snailz/grid.py`

    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    def to_csv(self) -> str:
        """Return a CSV string representation of the grid data.

        Returns:
            A CSV-formatted string containing the grid values without a header row,
            using Unix line endings (LF).
        """
        output = io.StringIO(newline="\n")
        writer = csv.writer(output, lineterminator="\n")
        for row in self.grid:
            writer.writerow(row)

        return output.getvalue()

## `Invperc`

Represent a 2D grid that supports lazy filling.

Source code in `src/snailz/grid.py`

     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    class Invperc:
        """Represent a 2D grid that supports lazy filling."""

        def __init__(self, depth: int, size: int) -> None:
            """Initialize the invasion percolation grid.

            Parameters:
                depth: The maximum value for grid cells
                size: The size of the grid (size x size)
            """
            self._depth = depth
            self._size = size
            self._cells = []
            for x in range(self._size):
                col = [random.randint(1, self._depth) for y in range(self._size)]
                self._cells.append(col)
            self._candidates = {}

        def __str__(self) -> str:
            """Convert to printable string representation.

            Returns:
                A string representation of the grid with '.' for unfilled cells
                and 'x' for filled cells, with each row on a separate line
            """
            rows = []
            for y in range(self._size - 1, -1, -1):
                rows.append(
                    "".join(
                        "." if self._cells[x][y] == 0 else "x" for x in range(self._size)
                    )
                )
            return "\n".join(rows)

        @property
        def cells(self) -> list[list[int]]:
            """Get the grid cell values.

            Returns:
                A 2D list of grid cell values
            """
            return self._cells

        def fill(self) -> None:
            """Fill the grid one cell at a time using invasion percolation.

            Starts at the center and fills outward, choosing lowest-valued adjacent cells,
            until reaching the border of the grid. After filling, inverts cell values.
            """
            x, y = self._size // 2, self._size // 2
            self._cells[x][y] = -self._cells[x][y]
            self.add_candidates(x, y)
            while True:
                x, y = self.choose_cell()
                self._cells[x][y] = -self._cells[x][y]
                if self.on_border(x, y):
                    break
            self.invert()

        def invert(self) -> None:
            """Flip cell values to final form.

            Converts negative values (filled cells) to their positive values
            and leaves non-negative values as 0 (unfilled)
            """
            for row in self._cells:
                for i in range(self._size):
                    row[i] = 0 if row[i] >= 0 else -row[i]

        def add_candidates(self, x: int, y: int) -> None:
            """Add unfilled cells adjacent to a filled cell as candidates for filling.

            Parameters:
                x: X-coordinate of the filled cell
                y: Y-coordinate of the filled cell
            """
            for ix in (x - 1, x + 1):
                self.add_one_candidate(ix, y)
            for iy in (y - 1, y + 1):
                self.add_one_candidate(x, iy)

        def add_one_candidate(self, x: int, y: int) -> None:
            """Add a single point to the set of candidates for filling.

            Parameters:
                x: X-coordinate of the candidate cell
                y: Y-coordinate of the candidate cell

            Note:
                Does nothing if the coordinates are outside the grid bounds
                or if the cell is already filled (negative value)
            """
            if (x < 0) or (x >= self._size) or (y < 0) or (y >= self._size):
                return
            if self._cells[x][y] < 0:
                return

            value = self._cells[x][y]
            if value not in self._candidates:
                self._candidates[value] = set()
            self._candidates[value].add((x, y))

        def choose_cell(self) -> tuple[int, int]:
            """Choose the next cell to fill using the invasion percolation algorithm.

            Returns:
                A tuple (x, y) of coordinates for the next cell to fill

            Note:
                Chooses the lowest-valued cell adjacent to already filled cells.
                If multiple cells tie for the lowest value, picks one at random.
                Updates the candidate set after selecting a cell.
            """
            min_key = min(self._candidates.keys())
            available = list(sorted(self._candidates[min_key]))
            i = random.randrange(len(available))
            choice = available[i]
            del available[i]
            if not available:
                del self._candidates[min_key]
            else:
                self._candidates[min_key] = set(available)
            self.add_candidates(*choice)
            return choice

        def on_border(self, x: int, y: int) -> bool:
            """Check if a cell is on the border of the grid.

            Parameters:
                x: X-coordinate of the cell
                y: Y-coordinate of the cell

            Returns:
                True if the cell is on any edge of the grid, False otherwise
            """
            size_1 = self._size - 1
            return (x == 0) or (x == size_1) or (y == 0) or (y == size_1)

### `cells` `property`

Get the grid cell values.

Returns:

Type | Description
--- | ---
`list[list[int]]` | A 2D list of grid cell values

### `__init__(depth, size)`

Initialize the invasion percolation grid.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`depth` | `int` | The maximum value for grid cells | required
`size` | `int` | The size of the grid (size x size) | required

Source code in `src/snailz/grid.py`

    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    def __init__(self, depth: int, size: int) -> None:
        """Initialize the invasion percolation grid.

        Parameters:
            depth: The maximum value for grid cells
            size: The size of the grid (size x size)
        """
        self._depth = depth
        self._size = size
        self._cells = []
        for x in range(self._size):
            col = [random.randint(1, self._depth) for y in range(self._size)]
            self._cells.append(col)
        self._candidates = {}

### `__str__()`

Convert to printable string representation.

Returns:

Type | Description
--- | ---
`str` | A string representation of the grid with '.' for unfilled cells
`str` | and 'x' for filled cells, with each row on a separate line

Source code in `src/snailz/grid.py`

     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    def __str__(self) -> str:
        """Convert to printable string representation.

        Returns:
            A string representation of the grid with '.' for unfilled cells
            and 'x' for filled cells, with each row on a separate line
        """
        rows = []
        for y in range(self._size - 1, -1, -1):
            rows.append(
                "".join(
                    "." if self._cells[x][y] == 0 else "x" for x in range(self._size)
                )
            )
        return "\n".join(rows)

### `fill()`

Fill the grid one cell at a time using invasion percolation.

Starts at the center and fills outward, choosing lowest-valued adjacent cells, until reaching the border of the grid. After filling, inverts cell values.

Source code in `src/snailz/grid.py`

    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    def fill(self) -> None:
        """Fill the grid one cell at a time using invasion percolation.

        Starts at the center and fills outward, choosing lowest-valued adjacent cells,
        until reaching the border of the grid. After filling, inverts cell values.
        """
        x, y = self._size // 2, self._size // 2
        self._cells[x][y] = -self._cells[x][y]
        self.add_candidates(x, y)
        while True:
            x, y = self.choose_cell()
            self._cells[x][y] = -self._cells[x][y]
            if self.on_border(x, y):
                break
        self.invert()

### `invert()`

Flip cell values to final form.

Converts negative values (filled cells) to their positive values and leaves non-negative values as 0 (unfilled)

Source code in `src/snailz/grid.py`

    139
    140
    141
    142
    143
    144
    145
    146
    147
    def invert(self) -> None:
        """Flip cell values to final form.

        Converts negative values (filled cells) to their positive values
        and leaves non-negative values as 0 (unfilled)
        """
        for row in self._cells:
            for i in range(self._size):
                row[i] = 0 if row[i] >= 0 else -row[i]

### `add_candidates(x, y)`

Add unfilled cells adjacent to a filled cell as candidates for filling.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`x` | `int` | X-coordinate of the filled cell | required
`y` | `int` | Y-coordinate of the filled cell | required

Source code in `src/snailz/grid.py`

    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    def add_candidates(self, x: int, y: int) -> None:
        """Add unfilled cells adjacent to a filled cell as candidates for filling.

        Parameters:
            x: X-coordinate of the filled cell
            y: Y-coordinate of the filled cell
        """
        for ix in (x - 1, x + 1):
            self.add_one_candidate(ix, y)
        for iy in (y - 1, y + 1):
            self.add_one_candidate(x, iy)

### `add_one_candidate(x, y)`

Add a single point to the set of candidates for filling.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`x` | `int` | X-coordinate of the candidate cell | required
`y` | `int` | Y-coordinate of the candidate cell | required

Note

Does nothing if the coordinates are outside the grid bounds or if the cell is already filled (negative value)

Source code in `src/snailz/grid.py`

    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    def add_one_candidate(self, x: int, y: int) -> None:
        """Add a single point to the set of candidates for filling.

        Parameters:
            x: X-coordinate of the candidate cell
            y: Y-coordinate of the candidate cell

        Note:
            Does nothing if the coordinates are outside the grid bounds
            or if the cell is already filled (negative value)
        """
        if (x < 0) or (x >= self._size) or (y < 0) or (y >= self._size):
            return
        if self._cells[x][y] < 0:
            return

        value = self._cells[x][y]
        if value not in self._candidates:
            self._candidates[value] = set()
        self._candidates[value].add((x, y))

### `choose_cell()`

Choose the next cell to fill using the invasion percolation algorithm.

Returns:

Type | Description
--- | ---
`tuple[int, int]` | A tuple (x, y) of coordinates for the next cell to fill

Note

Chooses the lowest-valued cell adjacent to already filled cells. If multiple cells tie for the lowest value, picks one at random. Updates the candidate set after selecting a cell.

Source code in `src/snailz/grid.py`

    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    def choose_cell(self) -> tuple[int, int]:
        """Choose the next cell to fill using the invasion percolation algorithm.

        Returns:
            A tuple (x, y) of coordinates for the next cell to fill

        Note:
            Chooses the lowest-valued cell adjacent to already filled cells.
            If multiple cells tie for the lowest value, picks one at random.
            Updates the candidate set after selecting a cell.
        """
        min_key = min(self._candidates.keys())
        available = list(sorted(self._candidates[min_key]))
        i = random.randrange(len(available))
        choice = available[i]
        del available[i]
        if not available:
            del self._candidates[min_key]
        else:
            self._candidates[min_key] = set(available)
        self.add_candidates(*choice)
        return choice

### `on_border(x, y)`

Check if a cell is on the border of the grid.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`x` | `int` | X-coordinate of the cell | required
`y` | `int` | Y-coordinate of the cell | required

Returns:

Type | Description
--- | ---
`bool` | True if the cell is on any edge of the grid, False otherwise

Source code in `src/snailz/grid.py`

    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    def on_border(self, x: int, y: int) -> bool:
        """Check if a cell is on the border of the grid.

        Parameters:
            x: X-coordinate of the cell
            y: Y-coordinate of the cell

        Returns:
            True if the cell is on any edge of the grid, False otherwise
        """
        size_1 = self._size - 1
        return (x == 0) or (x == size_1) or (y == 0) or (y == size_1)

## `grid_generate(params)`

Generate grid using invasion percolation.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`params` | `GridParams` | GridParams object containing depth, seed, size | required

Returns:

Type | Description
--- | ---
`Grid` | Grid object containing the generated grid and parameters

Source code in `src/snailz/grid.py`

    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    def grid_generate(params: GridParams) -> Grid:
        """Generate grid using invasion percolation.

        Parameters:
            params: GridParams object containing depth, seed, size

        Returns:
            Grid object containing the generated grid and parameters
        """
        invperc = Invperc(params.depth, params.size)
        invperc.fill()
        return Grid(grid=invperc.cells, params=params)