Skip to content

ctde_v0.terrains

terrains

Connectivity-safe crowded terrains for ctde_v0 (experiment-level).

These conform to zymera.worldgen.Terrain: walls(key, h, w) -> (H, W) bool mask (True = wall), a frozen dataclass with a pure-JAX method (runs inside the jitted reset, so NO python branching on traced values).

Every generator GUARANTEES the free space is one connected region. We don't generate-and-reject (not JAX-traceable); we construct connectivity: scatter obstacles, then a fixed-iteration BFS flood-fill from a central free seed turns every cell the wavefront can't reach into wall. The surviving free set IS the seed's 4-connected component — connected by definition, coverage well-posed (100% reachable), spawn always has free cells. The styles (random clutter / regular pillars / rooms+clutter) give the obstacle-field DIVERSITY; the fill makes each one solvable.

ConnectedClutter dataclass

ConnectedClutter(n_obstacles, fill_iters=0)

n_obstacles random wall cells, then flood-fill → connected free space. A well-posed version of :class:zymera.worldgen.RandomWalls for high density.

Pillars dataclass

Pillars(spacing=4, size=2, jitter=True)

A regular lattice of size×size obstacle blocks every spacing cells (parking-garage / forest of columns); corridors of width spacing-size run between them. Connected by construction; flood-fill trims any boundary scraps. Density ≈ (size/spacing)**2.

MixedCluttRooms dataclass

MixedCluttRooms(rooms=3, n_obstacles=40, door_w=2)

Vertical rooms (wide doors) PLUS scattered clutter, flood-filled → structured corridors and unstructured obstacles in one connected map.

RandomCrowded dataclass

RandomCrowded(members)

Per-reset MIXTURE — draws one member style each reset key (lax.switch, so only the drawn style computes). One policy thus trains across clutter + pillars + mixed in a single run → generalises over crowded-map DIVERSITY instead of overfitting one obstacle style. Members must be frozen/hashable.

connected_fill

connected_fill(wall, n_iter=None)

Wall off every free cell not reachable from the central seed → the remaining free space is a single 4-connected component.

The center cell is forced free and used as the seed (for moderate obstacle density it lies in the giant component). n_iter defaults to 2*(h+w), ample for clutter/pillars; the result is connected for ANY n_iter (too small just keeps a smaller blob — never disconnected).

Source code in experiments/ctde_v0/terrains.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def connected_fill(wall, n_iter=None):
    """Wall off every free cell not reachable from the central seed → the
    remaining free space is a single 4-connected component.

    The center cell is forced free and used as the seed (for moderate obstacle
    density it lies in the giant component). ``n_iter`` defaults to ``2*(h+w)``,
    ample for clutter/pillars; the result is connected for ANY ``n_iter`` (too
    small just keeps a smaller blob — never disconnected)."""
    h, w = wall.shape
    free = ~wall
    cr, cc = h // 2, w // 2
    free = free.at[cr, cc].set(True)                 # guarantee a free seed
    seed = jnp.zeros((h, w), bool).at[cr, cc].set(True)
    reach = _bfs_reach(seed, free, n_iter or 2 * (h + w))
    return ~reach                                    # everything unreached → wall

default_crowded_mix

default_crowded_mix(n_obstacles, pillar_spacing, pillar_size, rooms)

The standard training mixture: random clutter, pillar lattice, rooms+clutter.

Source code in experiments/ctde_v0/terrains.py
129
130
131
132
133
134
135
def default_crowded_mix(n_obstacles, pillar_spacing, pillar_size, rooms):
    """The standard training mixture: random clutter, pillar lattice, rooms+clutter."""
    return RandomCrowded((
        ConnectedClutter(n_obstacles=int(n_obstacles)),
        Pillars(spacing=int(pillar_spacing), size=int(pillar_size)),
        MixedCluttRooms(rooms=int(rooms), n_obstacles=int(n_obstacles) // 2),
    ))

n_components

n_components(wall)

Count 4-connected free components (host-side, exact).

Source code in experiments/ctde_v0/terrains.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def n_components(wall):
    """Count 4-connected free components (host-side, exact)."""
    h, w = wall.shape
    free = ~np.asarray(wall)
    seen = np.zeros_like(free)
    comps = 0
    for i in range(h):
        for j in range(w):
            if free[i, j] and not seen[i, j]:
                comps += 1
                stack = [(i, j)]
                seen[i, j] = True
                while stack:
                    r, c = stack.pop()
                    for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                        nr, nc = r + dr, c + dc
                        if 0 <= nr < h and 0 <= nc < w and free[nr, nc] and not seen[nr, nc]:
                            seen[nr, nc] = True
                            stack.append((nr, nc))
    return comps