Adjacency and Grids
Boardzilla provides a few tools to help determine adjacency or distance between game elements. This can be complicated since different games might have wildly different needs for measuring distance and adjacency. So what is provided in Boardzilla is intended to cover a few basic scenarios while affording flexibility for a variety of non-traditional concepts of adjacency.
It's also important that we distinguish between the concept of positioning from the perspective of the game's rules as opposed to positioning from the perspective of its appearance on screen. The two are related. If adjacency is important to the game's rules, keep reading. The relationship between rules and appearance will be explained. If however, the positioning is only important from a visual perspective, refer instead to Layout.
Types of grids
To understand adjacency, we need to talk about the grid on which we're playing. In Boardzilla there are two fundamentally different grid concepts that can be leveraged, fixed and flexible. Which one we use depends on the type of game.
Fixed
- Applies to Spaces
- Is usually visible
- Does not change size
- Useful for chessboards, Catan, etc.
Flexible
- Applies to Pieces
- An "imaginary" grid that can change position and size depending on its contents
- Handles irregularly shaped pieces that occupy multiple cells
- Useful for tile placement games, dominoes, etc.
Fixed grid
A fixed grid is a grid of Spaces that are created when we define our Game. These come in a few flavors: SquareGrid, HexGrid and ConnectedSpaceMap.
Square grids
For a typical chessboard, we would say:
import { SquareGrid } from '@boardzilla/core';
game.create(SquareGrid, 'chessBoard', { rows: 8, columns: 8});
This creates 64 Spaces and automitically sets row
and column
properties on
each. We can query these like any other property, e.g.
// the top-left corner
const corner = $.chessBoard.first(Space, { row: 1, column: 1 })!;
And we can now use adjacency and distance methods on these spaces. For example, to get the two spaces directly adjacent to the corner, we can say:
corner.adjacencies(Space);
// or
$.chessBoard.allAdjacentTo(corner, Space);
We can also measure distance, for example a knight's move is a distance of 3 spaces:
const knight = $.chessBoard.first(Space, { row: 3, column: 2 })!;
corner.distanceTo(knight); // 3
// or
$.chessBoard.distanceBetween(corner, knight);
We can query for other spaces with a certain distance as well. For example:
corner.withinDistance(5);
// or
map.allWithinDistanceOf(corner, 5, Space);
If diagonals need to be treated as adjacent, this can be done by setting
diagonalDistance
to some
number. The number will be the distance for diagonal moves, e.g.:
game.create(SquareGrid, 'chessBoard2', { rows: 8, columns: 8, diagonalDistance: 1.5 });
This affects both adjacency:
const corner = $.chessBoard2.first(Space, { row: 1, column: 1 })!;
corner.adjacencies(); // now 3 Spaces
and distance:
const corner = $.chessBoard2.first(Space, { row: 1, column: 1 })!;
const knight = $.chessBoard2.first(Space, { row: 3, column: 2 })!;
corner.distanceTo(knight); // 2.5
When we apply a visual layout to spaces that have been created
with rows and columns, the layout API automatically uses that row/column
information to layout the space. Practically speaking that means it is usually
unnecessary to define rows
and columns
for such a layout as the default will
follow the grid structure.
In rare cases when unpositioned items are put into the same layout as positioned grid items, the unpositioned ones will be fit around the grid.
Hex grids
In the case of hex grids, specify the grid style
. Essentially what this does
is create a grid with non-orthogonal rows and columns where adjacency applies in
6 directions, rather than 4 or 8. There are a few different shapes and row,
column configurations to choose from.
import { HexGrid } from '@boardzilla/core';
game.create(HexGrid, 'diamondBoard' { rows: 3, columns: 3 );
game.create(HexGrid, 'hexBoard' { rows: 5, columns: 5, shape: 'hex' );
Adjacency now applies in 6 directions, although the adjacent row, column pairs
will depend on which axes
was used.
game.create(HexGrid, 'hexBoard', { rows: 3, columns: 3, shape: 'hex' );
const middle = $.hexBoard.first(Space, { row: 2, column: 2 })!;
middle.adjacencies(Space); // 6 such Spaces
Distance calculations apply in the same way as square grids. However, there are never special diagonals in hex grids, and all 6 directions are treated equally.
By default, this creates spaces at every row and column starting from 1,1 to the
rows
and columns
specified. This means that the shape we get by default is
rhomboid. If some other shape is desired, set shape
to 'hex' or 'square'
These create adjacencies for use by the rules of the game. And visually it
creates a default grid that matches the style and shape of grid specified. These
can have layouts applied like other Spaces, however, these grids
only have a single layout for their Spaces. Rather than calling layout
to
layer on more layouts, you modify the base layout by calling
configureLayout
. See the
"hex" preset in the layout sandbox as an example.
Custom adjacency
For all other styles of adjacency, use the base class ConnectedSpaceMap. Using this we can add Spaces and connect them in whatever adjacency configuration is needed by calling connect. These can have custom distances applied to them to use for distance calculations. This is actually just a directed graph. There are too many possibilities to describe fully. A simple example would be creating a graph of spaces with travel distances:
const map = game.create(ConnectedSpaceMap, 'map');
const space1 = map.create(Space, 'space1');
const space2 = map.create(Space, 'space2');
const space3 = map.create(Space, 'space3');
map.connectTo(space1, space2, 2);
map.connectTo(space2, space3, 3);
map.connectTo(space1, space3, 6);
map.distanceBetween(space1, space3); // equals 5 using a path thru space2
Note that any connection is considered "adjacent" but the distance provided as
the 2nd argument is used in any queries or methods that measure distance, e.g.
distanceBetween
or
allWithinDistanceOf
.
Flexible Grid
The flexible grid is for placing Pieces on a space where the grid is not necessarily part of the board, or even a physical, visible thing. For example, when playing dominoes, you do not usually have an actual visible grid on the table, but an imaginary one is created as tiles are placed, which becomes the basis for determining if a domino is in a valid position based on adjacency to the other dominoes.
The class PieceGrid
is the basis for all flexible
grids. It is a grid that only directly contains Pieces and provides some other
special behaviour for the pieces placed in it. Any pieces in the PieceGrid must
have a row
and column
assigned to them, either by the game or the
player. Players normally place pieces onto the grid using the special
placePiece
. choice, which is specifically
designed for this, and sets the row
and column
(and optionally rotation
as
well).
Once a piece is placed on the PieceGrid, you can use the adjacency methods of the class to determine what pieces are adjacent to each other. This is just like the fixed grid, except for two very important characteristics:
- The grid can extend
- Pieces can have irregular shapes
Extendable Grid
When players choose to place dominoes, there is no limit to the imaginary grid
on the table. Everyone playing the game has probably had the experience of
getting up to the edge of the table, and needing to slide the entire structure
to make room. The imaginary grid of a PieceGrid is likewise essentially
infinite! You can set the rows
and columns
properties of a PieceGrid just
like the SquareGrid and HexGrid, but it will automatically grow beyond the
initial size as players place pieces. This behaviour can be disabled if you
actually want a limited playing field by setting extendableGrid
to false
.
Irregular Shapes
Pieces can be given irregular shapes by calling
setShape
. The shapes are represented as
strings with non-space characters to show filled in areas of the shape, and
spaces to show gaps. E.g. the squiggle tetris piece might look like this:
tetrisPiece.setShape(
'XX ',
' XX'
);
The shape of a piece does not normally matter much, expect when placed on a
PieceGrid
. When on a PieceGrid
, the piece will fill the appropriate number
of rows and columns as its size increases. The exact shape determines what is
adjacent to what, or what overlaps what and is therefore invalid.
The cells can also be labelled. This is useful when we want to query exactly which part of a piece is adjacent to another piece. For example in dominoes, we need to know if the adjacent dominoes have matching numbers. We can do so with labels, e.g.:
domino12.setShape(
'12'
);
domino23.setShape(
'23'
);
Now if we want to check the adjacency of domino12
and domino23
if they were
placed side-by-side, we can place them onto a PieceGrid
and then use the
adjacenciesByCell
method of the
grid.
dominoGrid.adjacenciesByCell(domino12, domino23);
// will return:
[
{
piece: domino23,
from: '2',
to: '2'
}
]
Like with a SquareGrid, we can also have diagonal
adjacent, by setting
diagonalAdjacency
, the
adjacenciesByCell
method will also return cells that touch only by a corner.
We can further add labels to the edges themselves for games where the tiles have distinctive edges that need be adjacent to other types of edges. Edge labels can be added to each of the 4 cardinal directions of each cell. Here's an example of 2 tiles from a game with tiles that go together to build a countryside.
bridge.setShape(
'AB',
'C '
);
bridge.setEdges({
A: {
up: 'road',
left: 'river',
},
B: {
right: 'river',
},
C: {
down: 'road',
},
});
// this is a simple one-celled shape, so we don't need cell labels
roadTurn.setEdges({
up: 'road',
right: 'road',
});
If these were placed together onto a PieceGrid named countryGrid
top-to-bottom, like this:
We could check if the edges match using the
adjacenciesByEdge
method of
countryGrid
, e.g.:
countryGrid.adjacenciesByEdge(bridge));
// will return:
[
{
piece: roadTurn,
from: 'river',
to: 'river'
}
]
The tiles starter template has a simple example of using the PieceGrid for tile placement that can be used as a starting point.