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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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)
    seed: int = Field()
    size: int = Field(gt=0)

    model_config = {"extra": "forbid"}

Grid

Bases: BaseModel

Keep track of generated grid.

Source code in src/snailz/grid.py
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 = utils.csv_writer(output)
        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
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 = utils.csv_writer(output)
    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)