# Levels as text: `textGrid` and `gridText`

Draw a map as text, one character per cell, and say what each character is in a legend. The
text is the map, its collision, its spawns and goals, all at once; `gridText` prints any grid
back as text, so you (or an agent) can *see* a level in a log or a failing test. Both are in
`/core` (three-free: rules, bots and tests use them headless).

## Contents

1. [The format](#1-the-format)
2. [The legend](#2-the-legend)
3. [Using the level](#3-using-the-level)
4. [Recipe: an arena](#4-recipe-an-arena)
5. [Recipe: a heightmap island with scattered trees](#5-recipe-a-heightmap-island-with-scattered-trees)
6. [Recipe: a multi-floor first-person map](#6-recipe-a-multi-floor-first-person-map)
7. [Recipe: a symmetric team map](#7-recipe-a-symmetric-team-map)
8. [Recipe: your own meanings (grid games, zones)](#8-recipe-your-own-meanings-grid-games-zones)
9. [Windows and bushes: sight](#9-windows-and-bushes-sight)
10. [Seeing it: `gridText`](#10-seeing-it-gridtext)
11. [Pitfalls](#11-pitfalls)

## 1. The format

```ts
import { B, textGrid } from '@voxelparty/sdk/core';

const level = textGrid(`
  ##########
  #S......G#
  #..##....#
  ##########
`, { '#': B.STONE, S: 'spawn', G: { block: B.GOLD, mark: 'goal' } });
```

- **Directions:** a line's first character is x = 0, x grows to the right; the first line is
  z = 0 (the far side, −z) and z grows down the page, towards the camera. As you'd draw a map.
- **Layers** are y, from the ground up: `textGrid([ground, firstFloor, roof], legend)`. One string
  is one layer.
- Indentation common to every line is dropped, and blank lines before and after a layer. Short
  lines are padded with air. Blank lines *inside* a layer are rows of air.
- `' '` and `'.'` are air unless the legend says otherwise (`'.': B.GRASS` is allowed).
- A character the legend doesn't have is an error naming it, its line and column and layer.
- Options: `{ seed, mirror: 'x' | 'z' | 'xz', swap: { r: 'b' }, shareSeam }` (sections 5 and 7).

## 2. The legend

Each character is one of:

| Entry | Means |
|---|---|
| `B.STONE`, `ids.WALL` | That block. `0` is air. |
| `'spawn'` | A mark: air here, and the cell is in `level.marks.spawn`. |
| `[B.STONE, B.DIRT, B.GRASS]` | A column, bottom up, starting at this layer. |
| `TREE` (any `Structure`) | A prefab (a `readVox` model, a `world.capture`, another `textGrid`), stamped centred on the cell, its bottom on this layer. Its air doesn't carve. |
| `{ block, height?, top?, meta?, mark?, prefab?, turns? }` | Any mix: `{ block: B.STONE, height: 3 }` a wall 3 tall; `{ block: B.DIRT, top: B.GRASS, height: 4 }` a hill; `{ block: B.GRASS, mark: 'spawn' }` grass with a spawn **standing on it** (the mark is the cell above the column); `{ block: B.GRASS, prefab: TREE, turns: 1 }` a tree on grass; `meta` is the top block's (a `tint` block's team, a turning top's direction). |
| `(at) => entry` | A function of the cell: `at.x`, `at.y`, `at.z`, `at.char`, and `at.rand()` (seeded by the cell and `seed`: the same map on every client). Return any entry above, or null for air. |

Marks are typed from the legend: with `S: 'spawn'`, `level.marks.spawn` is a `GridPos[]` (never
undefined, empty if the text has none). `level.mark('spawn')` is the first one, and throws naming
the marks there are if there's none.

## 3. Using the level

A `TextGrid` **is** a `Volume` (ids and metas, `get`, `set`) and a `Structure`, so it goes wherever
those do. Its cells are 1 unit, so place it with a cell offset:

```ts
world.stamp(level, 0, 4, 0);                                 // into a World (vp docs world): cell x, y, z
grid.addVolume(level, [-5, 0, -2], blockIsSolid);           // into a VoxelGrid (vp docs fps): world x, y, z
addVoxelMeshes(scene, level, engine.mats, [-5, 0, -2]);      // drawn: terrain meshes on mats.solid/water/cross
const s = level.mark('spawn');                               // { x, y, z } cells
body.x = -5 + s.x + 0.5; body.y = s.y; body.z = -2 + s.z + 0.5;   // the middle of that cell, feet on its floor
level.char(x, y, z);                                          // the text's character at a cell (' ' outside)
```

Build it at module level (or from `link.seed`) in `rules.ts`: every client gets the same level,
and tests can use it.

## 4. Recipe: an arena

```ts
export const ARENA = textGrid(`
  ~~~~~~~~~~~~~~
  ~############~
  ~#1........2#~
  ~#..##..##..#~
  ~#..#....#..#~
  ~#....**....#~
  ~#..#....#..#~
  ~#..##..##..#~
  ~#3........4#~
  ~############~
  ~~~~~~~~~~~~~~
`, {
  '~': B.WATER,
  '#': { block: B.COBBLE, height: 2 },         // walls two blocks tall
  '*': { block: B.GOLD, mark: 'prize' },       // the prize sits on gold
  1: 'spawn', 2: 'spawn', 3: 'spawn', 4: 'spawn',
});
ARENA.marks.spawn;   // four cells, in text order: 1, 2, 3, 4
```
The floor is somewhere else (an `Island`, or a floor layer below: `textGrid([FLOOR, ARENA_TEXT], …)`).

## 5. Recipe: a heightmap island with scattered trees

Digits as heights read like a contour map:
```ts
const H = (n: number) => ({ block: B.DIRT, top: B.GRASS, height: n });
const TREE = textGrid([`...\n.L.\n...`, `...\n.L.\n...`, `LLL\nLLL\nLLL`, `.L.\nLLL\n.L.`], { L: B.LEAVES_OAK });
TREE.set(1, 0, 1, B.LOG); TREE.set(1, 1, 1, B.LOG);   // a trunk: prefabs are just Volumes

export const ISLAND = textGrid(`
  ~~~~~~~~~~~~~~~~
  ~~~111122111~~~~
  ~~11222233211~~~
  ~11223344332t1~~
  ~1t2334554321~~~
  ~~1122333321t1~~
  ~~~~11221111~~~~
  ~~~~~~~~~~~~~~~~
`, {
  '~': B.WATER,
  1: H(1), 2: H(2), 3: H(3), 4: H(4), 5: H(5),
  t: ({ rand }) => ({ ...H(1), prefab: TREE, turns: Math.floor(rand() * 4) }),   // a tree, turned at random
}, { seed: link.seed });
```
- The function sees `rand()`, seeded by the cell and `seed`: the same island on every client, a
  different one per seed. Return `null` for air.
- Scatter without marking every spot: `'.': ({ rand }) => (rand() < 0.08 ? { ...H(1), prefab: TREE } : H(1))`.
- `gridText(ISLAND, { view: 'heights' })` shows the terrain back as heights.

## 6. Recipe: a multi-floor first-person map

Walls are columns, so the ground floor is one layer; a storey above is its own grid (a floor slab
layer, then its walls), placed one storey up:
```ts
const WALL = { block: ids.WALL, height: 3 };
export const GROUND = textGrid(`
  ############
  #1...#.....#
  #....D..^..#
  #....#.....#
  ############
`, { '#': WALL, D: 0, '^': ids.STAIRS, 1: 'spawn' });   // D: a doorway (air)
export const UPPER = textGrid([`
  ffffff
  ffffff
  ffffff
`, `
  ==GG==
  =2...=
  ======
`], { f: ids.FLOOR, '=': WALL, G: { block: ids.GLASS, height: 2 }, 2: 'spawn' });

const at: [number, number, number] = [-6, 0, -3];
grid.addVolume(GROUND, at);                            // half-unit collision cells: a voxel fills 2 × 2 × 2
grid.addVolume(UPPER, [at[0], at[1] + 3, at[2]]);      // one storey (3) up
addVoxelMeshes(scene, GROUND, engine.mats, at);
addVoxelMeshes(scene, UPPER, engine.mats, [at[0], at[1] + 3, at[2]]);
```
Marks are in each grid's own cells: add the same offset to get world positions.

## 7. Recipe: a symmetric team map

Draw one half; `mirror` adds the other, flipped, and `swap` trades the teams' characters in the copy:
```ts
export const DUEL = textGrid(`
  ..........
  .c####....
  .crr#>#...
  .c####....
  ..........
`, {
  '#': ISLAND, c: [...ISLAND, B.COBBLE],
  r: { block: [B.STONE, ids.GOAL], meta: 1, mark: 'redGoal' },
  b: { block: [B.STONE, ids.GOAL], meta: 2, mark: 'blueGoal' },
  '>': { block: ISLAND, mark: 'redSpawn' }, '<': { block: ISLAND, mark: 'blueSpawn' },
}, { mirror: 'x', swap: { r: 'b', '>': '<' } });
```
- `mirror: 'x'` puts the flipped copy to the right (width doubled), `'z'` below, `'xz'` four ways.
- `shareSeam: true` makes the half's last column the middle line (an odd width with a centre lane).
- A function entry in the copy gets its twin's `rand()`: a random map stays fair.
- `vp init --blocks` draws its whole map this way (map.ts): the goals and spawns are marks.

## 8. Recipe: your own meanings (grid games, zones)

A level doesn't have to be blocks. `level.char(x, y, z)` is the text's character at any cell, and
marks collect cells, so a 2D grid game can use the text as its board:
```ts
const BOARD = textGrid(`
  ###########
  #S..i..i..#
  #.##i##i#.#
  #..iiiii..#
  ###########
`, { '#': ids.WALL, i: { block: ids.ICE, mark: 'ice' }, S: 'start' });
const slippery = (x: number, z: number) => BOARD.char(x, 0, z) === 'i';
```
Your rules read `char` (or `get` for the block); the view meshes the same level.

## 9. Windows and bushes: sight

`VoxelGrid.sees` stops at **opaque** cells, which aren't always the solid ones:
```ts
// A World: say it in the block's rules.
GLASS: { top: 'glass', opaque: false },               // you bump into it, you see through it
BUSH: { top: 'bush', solid: false, opaque: true },    // you walk through it, it hides you
// A plain VoxelGrid: its tables, by cell value.
grid.opaque[M.GLASS] = 0;
grid.solid[M.BUSH] = 0;
grid.ray(ox, oy, oz, dx, dy, dz, 30, undefined, grid.opaque);   // how far you can see
```
In a text map it's just a character: `'=': ids.GLASS`.

## 10. Seeing it: `gridText`

```ts
console.log(gridText(level));                          // from above: each column's top
console.log(gridText(world, { view: 'heights' }));     // . none, 1–9, then a–z
console.log(gridText(level, { view: 2 }));              // one layer, as textGrid reads it
console.log(gridText(level, { view: 'all' }));          // every layer, each under "y N"
console.log(gridText(world, { legend: LEVEL.legend, area: [0, 0, 20, 12] }));   // a World in your characters, a corner of it
```
- A `TextGrid` prints in its own characters: `gridText(level, { view: 0 })` is the text you wrote
  (with `.` for air). A cell changed since (a broken block) prints by its block.
- Anything else (a `World`, a `Volume`, a `Structure`, a `VoxelGrid`) prints through `legend`
  (single blocks, and columns by their top block and meta). Blocks it doesn't name get a character
  each, listed in a key underneath: `(# stone (3), = 200)`.
- In tests: `expect(gridText(w, { view: 'heights' })).toBe(EXPECTED)` makes a failure show the map.
  After an explosion, print the world and look.

## 11. Pitfalls

- **Layers are one cell tall.** Layer 1 is the cells just above layer 0, not the next storey: tall
  walls are columns (`height: 3`), and a storey above is its own grid placed higher (recipe 6).
- **Marks on blocks stand on top.** `{ block: B.GRASS, mark: 'spawn' }` marks the cell above the
  grass (where a body stands); a bare `'spawn'` marks the cell itself.
- **Air never carves.** A `.` over a column from a lower layer leaves the column. To cut something
  out, `level.set(x, y, z, 0)` after building.
- **Prefabs are clipped** at the level's sides (the top grows to fit them). Leave a margin.
- **Prefab turns turn positions, not metas:** a turning top's direction inside a prefab stays as it
  was. `world.stamp(structure, x, y, z, turns)` turns those too (the `World` knows which blocks turn).
- **Cells are ids 0–255,** as everywhere: a game's own blocks from `blockIds(BLOCKS)` / `useGameAssets`.
