# Board geometry and own functions

A model can describe space. A **board** (`boards`) says which cells exist, which cells are
neighbours, and where walls and holes are. **Board functions** in the expression language compute
with it: neighbours, distance, sliding until the first piece, cheapest path, line of sight, area
patterns. **Own functions** (`functions`) bundle rules that come back often, such as "is this
square attacked" in chess or "which cell does this monster walk to" in a skirmish game.

Two fully executable examples, both playable in the [player](https://tabletwin.com/play/) with
their board drawn from the geometry:

| Model | What it shows |
|---|---|
| [chess.yaml](https://tabletwin.com/models/games/chess.yaml) | Square board with 8 neighbours and letter names; all move generation in `functions`, including castling, en passant and promotion |
| [hex-skirmish.yaml](https://tabletwin.com/models/docs/examples/hex-skirmish.yaml) | Hex map with holes, obstacles and difficult terrain; monsters that pick a target, walk and check line of sight; an area attack with rotation |

## 1. Boards

```yaml
boards:
  board: {kind: square, width: 8, height: 8, names: letters, neighbours: 8}
  map:
    kind: hex
    width: 7
    height: 5
    missing: ["3,0", "3,1", "2,3", "1,4"]     # holes: these cells do not exist
    walls: [["0,0", "1,0"]]                    # a wall between two neighbours
```

| Field | On | Default | Meaning |
|---|---|---|---|
| `kind` | all | `square` | `square` or `hex` |
| `width`, `height` | square (required); hex as a rectangle | – | Integers |
| `names` | square | `xy` | `xy`: cell `"3,4"`. `letters`: cell `"e4"` (column `a`…`z`, row from 1; at most 26 columns) |
| `neighbours` | square | 4 | `4` (orthogonal) or `8` (also diagonal) |
| `radius` | hex | – | A hexagonal board around `0,0` with this radius |
| `cells` | hex | – | A list of cells `"q,r"` (any shape) |
| `missing` | all | – | Cells that do not exist: you cannot stand on, move through or see through them. Each must be a cell of the board |
| `walls` | all | – | Pairs of neighbouring cells with a wall between them: blocks `step`, `ray`, `neighbours`, paths and sight, not `leap`. On a square board only orthogonal neighbours can have a wall |
| `label`, `note` | all | – | For people |

A hex board uses `cells` if given, else `radius`, else `width` and `height`.

**Coordinates.** Square: `x` to the right, `y` up; cell `a1` (or `"0,0"`) is `x = 0, y = 0`.
Hex: axial coordinates `q,r` with pointy tops; `r` grows downward. With `width`/`height`, odd rows
are shifted half a cell to the right so the map is a rectangle; the cell names stay axial (row 2
starts at `q = -1`, row 4 at `q = -2`).

**Directions.** Square with 4 neighbours: `n e s w`; with 8: `n ne e se s sw w nw`; `n` is
`y + 1`. Hex, clockwise from east: `e se sw w nw ne` (`e` is `q + 1`, `se` is `r + 1`, `ne` is
`q + 1, r − 1`). Wherever a direction is expected you may also give a vector `[dx, dy]` (hex:
`[dq, dr]`) of integers.

**Cell order.** `cells(b)` lists the cells row by row: square from `y = 0` up, `x` ascending
(`a1, b1, …`); hex rectangle from `r = 0` down; hex radius from `r = −radius`, `q` ascending;
hex `cells` in the given order. Holes are left out.

A board is fixed during play. What stands on it lives in the state: a variable
`{cell: piece}`, or instances in a zone with a `cell` attribute.

**Errors in a board** are validator errors, and `NotExecutable` when the engine loads the model:
`board 'b': kind 'x'; use square or hex`, `... width must be an integer, not None`,
`... names 'x'; use xy or letters`, `... names letters allows at most 26 columns`,
`... neighbours 6; use 4 or 8`, `... cells must be a list of 'q,r' cells`,
`... hex cell 'x'; write 'q,r' with integers`, `... missing names unknown cell 'x'`,
`... cell 'x' appears twice`, `... wall [...]; write [cell, cell]`,
`... wall at unknown cell 'x'`, `... wall between 'a' and 'b'; those cells are not adjacent`.
The validator also warns about a board whose name never appears (in quotes) in any expression.

## 2. Board functions

Available in every expression when the model has `boards`. The first argument is always the
board name. Optional arguments (`?`) default to `None` and may be given by name:
`path('map', a, b, costs=TERRAIN)`.

| Function | Returns | Meaning |
|---|---|---|
| `cells(board)` | list | All cells, in cell order |
| `directions(board)` | list | The direction names of this board |
| `xy(board, cell)` | tuple | Coordinates `(x, y)` (hex: `(q, r)`) |
| `cell(board, x, y)` | cell or `None` | The cell at these integer coordinates; `None` off the board or on a hole |
| `step(board, cell, direction)` | cell or `None` | One step; `None` off the board, onto a hole, or through a wall |
| `leap(board, cell, direction)` | cell or `None` | Like `step`, but over walls (a knight's move is `[1, 2]`) |
| `ray(board, cell, direction, occupied?)` | list | Cells in that direction up to the edge, a hole or a wall, or up to and including the first cell in `occupied` |
| `neighbours(board, cell)` | list | Adjacent cells, without walls and holes, in the order of `directions` |
| `distance(board, a, b)` | integer | Hex: steps. Square: with 4 neighbours `abs(dx) + abs(dy)`, with 8 `max(abs(dx), abs(dy))`. Ignores walls and holes |
| `path(board, start, goal, blocked?, costs?, through?)` | list or `None` | Cheapest path: the cells after `start`, ending with `goal`. `[]` if `start == goal` |
| `path_cost(board, start, goal, blocked?, costs?, through?)` | number or `None` | Cost of that path; `0` if `start == goal` |
| `reachable(board, start, points, blocked?, costs?, through?)` | list | Cells reachable with at most `points`, cheapest first, without `start` |
| `line_of_sight(board, a, b, corners?, blockers?)` | bool | Is there a clear line from `a` to `b` |
| `shape(board, cell, pattern, turns?)` | list | The cells of a pattern placed at `cell`, rotated `turns` times clockwise |

`occupied`, `blocked`, `through` and `blockers` are anything that supports `in`: a list, a set,
or a dict (its keys).

**Paths.** Dijkstra over `neighbours`.
- `blocked`: cells you may not enter (enemies, obstacles).
- `through`: cells you may pass but not end on (allies).
- `costs`: `{cell: cost}`, the cost of entering that cell (default 1; must be a number ≥ 0),
  e.g. 2 for difficult terrain.
- `path` and `path_cost` give `None` if `goal` is unreachable, or is in `blocked` or `through`
  (unless `start == goal`). `start` itself is never checked against `blocked`.
- `reachable` leaves out `start` and the `through` cells.
- On equal cost the path found first wins (neighbours in the order of `directions`), so the
  result is always the same.

**Line of sight.** The line runs from centre to centre. With `corners` true, the engine tries
lines from every corner of `a` to every corner of `b` (corners moved 5% toward the centre) and
there is sight if any one of them is clear. A line is blocked if a sample point falls off the
board or on a hole, if it crosses a wall between two consecutive cells, or if it enters a cell in
`blockers` (other than `a` and `b`). The line is sampled 8 times per unit of length (a square
cell is 1 wide; hex centres are √3 apart), so a line that grazes a corner exactly does not count
as blocked. `a == b` is always `True`.

**Patterns.** A list of directions or vectors, relative to `cell`. `[[0, 0], 'e', 'se']` is a
triangle of three hexes. `turns` rotates clockwise by 60° (hex) or 90° (square); negative values
rotate counter-clockwise. Cells off the board or on holes are left out; duplicates appear once;
walls do not matter. All rotations: `[shape('map', t, P, k) for k in range(6)]`.

**Errors.** Board function errors are `ValueError`s and appear like any expression error
(`error evaluating '...': ValueError: ...`), for example:
`cell 'z9' does not exist on board 'map'`, `direction 'up' does not exist on board 'map'`,
`direction [1]: a vector is [dx, dy] with integers`, `board 'mpa' does not exist`,
`x must be an integer, not 1.5`, `costs must be a dict {cell: cost}, not [...]`,
`cost of '2,2' must be a number ≥ 0, not -1`, `reachable: points must be a number, not None`,
`shape: pattern must be a list of vectors or directions, not 'e'`, and for arguments:
`path() is missing argument 'goal'`, `path() has no argument 'cost'`,
`path() got 'blocked' twice`, `step() takes at most 3 arguments, not 4`.

With English names, take care not to hide a board function: a local name, variable or zone named
`cell`, `step` or `path` hides the function of that name (see
[expressions.md § Names in scope](https://tabletwin.com/docs/expressions/#6-names-in-scope)). An own function may not
take such a name.

## 3. Own functions

```yaml
functions:
  enemy:
    params: [k]
    x: "'b' if k == 'w' else 'w'"
  in_reach:
    text: "within range (around walls) and in sight"
    params: [a, b, reach]
    let:
      cost: "path_cost('map', a, b) if distance('map', a, b) <= reach else None"
    x: "cost is not None and cost <= reach and line_of_sight('map', a, b, True)"
```

| Field | Meaning |
|---|---|
| `params` | Parameter names (`^[A-Za-z][A-Za-z0-9_]*$`). `name?` is optional (default `None`). Call by position or by name |
| `let` | Helper values `{name: expression}`, computed top to bottom; each sees the parameters and earlier `let`s |
| `x` | The result (required) |
| `text`, `note` | For people |

A function sees its parameters, its `let`s, the constants, `content`, the state (zones,
variables, derived values), `modified`, the board functions and the other functions (itself too:
recursion is allowed up to 100 calls deep). It does **not** see the local names of its caller
(`actor`, `self`, bound names): pass them as arguments. A parameter hides a variable or zone of
the same name. A function changes nothing; ops do that.

Functions work wherever an expression is allowed: `pre_x`, `options_x`, `value_x`, end
conditions, `init_x`, tactics. The name must not clash with a constant, variable, zone, derived
value, builtin, board function (when the model has boards) or reserved name; the validator and
the engine both reject it (`function 'x': that name already exists`). Other validator checks:
`no x`, `params must be a list of names`, `parameter 'x' is not a name`,
`let must be a dict {name: expression}`, and a warning for a function that is never called.

Argument errors look like those of board functions: `in_reach() is missing argument 'reach'`.

## 4. Patterns

**Sliding pieces.** With a variable `pieces` of the form `{cell: piece}` (a piece is text like
`'wR'`, colour first) and `colour` the side to move, all rook moves from `c`:

```python
[n for d in ['n', 'e', 's', 'w'] for n in ray('board', c, d, pieces) if pieces.get(n, '-')[0] != colour]
```

`ray` stops on the first occupied cell and includes it, so an own piece is filtered out and an
enemy piece can be captured. A knight: `[n for v in [[1, 2], [2, 1], [2, -1], [1, -2], [-1, -2],
[-2, -1], [-2, 1], [-1, 2]] for n in [leap('board', c, v)] if n is not None]`.

**Is a cell attacked?** Look outward from the cell: knight leaps, king neighbours, pawn
diagonals, and per line the first piece (`ray(...)[-1]`). A function `legal` then drops the
moves after which the own king is attacked. [chess.yaml](https://tabletwin.com/models/games/chess.yaml)
does it this way.

**A monster walking to its target.**
1. Attack cells: empty cells from which the monster can hit a hero (`in_reach` above).
2. Focus: the hero with the cheapest attack cell (`path_cost` with enemies as `blocked` and
   allies as `through`).
3. Destination: the cheapest attack cell within its movement (`reachable`), else the cell that
   gets closest to one.

**Area attack.** For each cell in range and in sight, and each rotation, the targets in
`shape(...)`; count each hit once.

```python
[[c for c in shape('map', t, AREA, k) if c in occupied] for t in targets for k in range(6)]
```

(`targets` being the cells in range and in sight, `AREA` a pattern constant and `occupied` the
cells with monsters. Sets have no `&`; filter with `in` instead.)

[hex-skirmish.yaml](https://tabletwin.com/models/docs/examples/hex-skirmish.yaml) works these out
in full.

## 5. Limits

- On a square board with 8 neighbours a wall only blocks the orthogonal step across it; a
  diagonal step past the end of a wall is still possible.
- Line of sight is sampled: a line that crosses less than an eighth of a unit through the corner
  of a hole can be missed.
- Boards are fixed. A map that changes during play (a door that opens) is modelled with a
  variable passed as `blocked` or `blockers`.
