Skip to content

Custom Tiles & Maps

Beyond generating and loading maps, PGTG lets you replace the content of tiles: the wall layout of each road piece, where obstacles can be generated, and how traffic flows.

The three tile data files

Each is an optional path passed to the constructor. When omitted, PGTG uses its packaged defaults.

Constructor argument Describes Default file
tiles The wall/exit layout of every tile shape. pgtg/data/tiles/tiles.json
obstacle_masks Named cell patterns used to stamp obstacles onto a tile. pgtg/data/tiles/obstacle_masks.json
traffic_lanes The lanes and spawn points cars use in each tile shape. pgtg/data/tiles/traffic_lanes.json
from pgtg import PGTGEnv

env = PGTGEnv(
    tiles="my_tiles.json",
    obstacle_masks="my_obstacle_masks.json",
    traffic_lanes="my_traffic_lanes.json",
)

Start from the defaults

The most reliable way to author these files is to copy the packaged defaults and edit them. They live in pgtg/data/tiles/ and document the exact expected structure. Both JSON and YAML are accepted.

Edit them visually

Writing these grids by hand is fiddly. The Tile Editor (WIP) is a companion visual tool that paints walls and exits, draws traffic routes by dragging, and validates the result before an environment loads it.

Tile dimensions must be consistent

Tile size is inferred from the provided files (the packaged defaults are 9×9 cells). All three datasets in use must share the same dimensions, so passing tiles with non-default dimensions requires matching obstacle_masks and traffic_lanes files. PGTG validates this at construction time.

Changing the tile size

Tile size isn't a separate setting but is instead inferred from the grids in the files you pass. To use a different size (bigger, smaller, or non-square), write tiles, obstacle_masks, and traffic_lanes files whose grids all share the new width and height. PGTG then picks it up automatically, and every dimension derived from it (the map observation planes, the position bounds, rendering, …) follows along. This is the same mechanism whether you're tweaking one shape or replacing the entire tile set.

The example below shrinks the default 9×9 tiles down to a 5×5 east-west straight tile (exit tuple 0,1,0,1).

tiles.json
{
  "0,1,0,1": [
    [["wall"], ["exit west"], ["exit west"], ["exit west"], ["wall"]],
    [["wall"], [], [], [], ["wall"]],
    [["wall"], [], [], [], ["wall"]],
    [["wall"], [], [], [], ["wall"]],
    [["wall"], ["exit east"], ["exit east"], ["exit east"], ["wall"]]
  ]
}

obstacle_masks.json and traffic_lanes.json must use matching 5×5 grids (an empty 5×5 grid works for both if this tile has no obstacles or traffic lanes):

obstacle_masks.json
{
  "blob": [
    [[], [], [], [], []],
    [[], [], [], [], []],
    [[], [], [], [], []],
    [[], [], [], [], []],
    [[], [], [], [], []]
  ]
}
from pgtg import PGTGEnv

env = PGTGEnv(
    map_path="path/to/a_map_using_only_0_1_0_1_tiles.json",
    tiles="tiles.json",
    obstacle_masks="obstacle_masks.json",
    traffic_lanes="traffic_lanes.json",
)
env.tile_width, env.tile_height  # (5, 5)

Cover every tile shape your maps use

A real map needs more than one exit tuple. Build out the full set of tile shapes, not just the single shape shown here, and keep obstacle_masks covering every mask name your maps or procedural generation reference.

tiles

Keys are exit tuples written as a comma-joined string "N,E,S,W" (each 0/1); values are a W×H grid of cells (9×9 in the packaged defaults), where each cell is a list of feature strings. The important markers are "wall" for impassable cells and "exit north" / "exit east" / "exit south" / "exit west" for the cells that connect to a neighboring tile; an empty list is open road.

{
  "1,0,0,0": [
    [
      ["wall"],
      ["wall"],
      ["wall"],
      ["wall"],
      ["wall"],
      ["wall"],
      ["wall"],
      ["wall"],
      ["wall"]
    ],
    [
      "... one list per column; the north exit sits at the START of the middle columns ..."
    ],
    [["exit north"], [], [], [], [], [], ["wall"], ["wall"], ["wall"]]
  ],
  "0,1,0,0": ["..."]
}

obstacle_masks

Keys are mask names; values are a W×H grid where each cell is ["obstacle"] (part of the mask) or [] (not). Masks decide which cells of a tile an obstacle covers. The built-in mask names are blob, small_blob, chess_field, reverse_chess_field, top_half, bottom_half, left_half, and right_half (plus directional traffic_light_* masks). A map file can reference a mask by name via a tile's "obstacle_mask" field. Randomly generated maps with obstacles pick from the built-in names, so a custom masks file used with them must define those names too.

traffic_lanes

Keys are exit tuples (as in tiles); values are a W×H grid where each cell may contain a lane marker such as "car_lane north south" (a lane with a flow direction and orientation) and/or "car_spawner" (a cell where new cars can appear). Empty cells carry no lane. The file must cover every exit tuple the map uses.

Authoring obstacles inside a map file

Fixed map files can pin obstacles to specific tiles by adding two fields to a tile entry:

{
  "exits": [1, 1, 0, 0],
  "obstacle_type": "ice",
  "obstacle_mask": "blob"
}
  • obstacle_type"ice", "sand", "broken_road", "traffic_light", or the registered name of a custom obstacle.
  • obstacle_mask — a mask name from your obstacle_masks file.

This allows building fully deterministic evaluation scenarios.