Board Structure
The Game is a singleton class that is declared as the first step of creating a
game. Creating a Boardzilla game automatically creates this class in
game/index.ts
which can have properties and methods added to it. This class
extends the base Game class.
The Game contains Spaces (fixed regions) and Pieces (movable game objects). This is essentially a hierarchy, with your game instance at the top of this hierarchy and the spaces and pieces placed within in it. For example, a Game may have spaces for each player's tableau, and inside those are spaces for the player's hand, and inside those are their cards. All of these spaces and pieces are called the Elements of the game. Elements in Boardzilla always have a parent-child relationship with each other.
In the context of an actual game board, the Spaces and Pieces in this diagram might look something like this:
Here the Spaces are grey and the Pieces are blue. Spaces can contain other spaces and pieces. For example the Tableau here contains the Hand and the Hand contains the Cards. Pieces can also contain other pieces, e.g. as when placing tokens onto a card.
Subclassing
Typically, a game will declare a few classes for your various game pieces,
e.g. Cards, Tokens and the like. Each of these will be a subclass of
Piece. These subclasses can add properties and methods
that you can use in the rules of game. E.g. a Card
class, that might have
suit
and number
properties, and special methods like isTrump()
.
export class Card extends Piece<MyGame> {
suit: "S" | "H" | "D" | "C";
number: number;
isTrump() {
return this.suit === this.game.trump;
}
}
You can add any properties and methods you like. Properties added will also be visible to the player unless hidden (See visibility).
There are some restriction for what type your properties can be in order to be serializable for the player. They must be one of:
- number
- string
- boolean
- GameElement (i.e. any Space or Piece)
- Player
- any array or object containing any combination the above
Notably they cannot be instances of some other class or arrow functions (normal methods are of course fine). Any such properties will throw an error.
Spaces can be subclassed as well. This is less common, but helpful if you have several spaces of a particular type that have special properties or behavior. There are also several builtin subclasses of Space for specialized grid and adjacency rules. See Adjacency and Grids.
Defining subclasses like this also makes it easy to customize their appearance later and give the different classes of Pieces entirely different visuals.
When subclassing Space
and Piece
, your game class must be used as the
generic for the type, e.g.:
export class Token extends Piece<MyGame> {
...
}
Querying
Accessing the various Spaces and Pieces of the game is done using the Query API on the game class or on the spaces and pieces you add. The two most important methods are:
all
Search the tree recursively and return all matchesfirst
Search the tree recursively and return only the first match
In the example tree above, calling game.all(Piece)
would return the two cards
at the bottom of the tree. If we used the Card class above, we could also have
used game.all(Card)
to return the same thing but typed correctly to the Card
class. We can then also search by name, e.g. game.first(Card, '2C')
to return
the Card named '2C', or add properties to the search, e.g. game.first(Card, { number: 1 })
to return the first ace in the game.
Any methods that return lists of elements, like all
, actually return an
ElementCollection. This is an Array-like
class that can be treated like an array but also contains many other methods.
Note that first
can return undefined
if no matching element is found. When using
first
, you will frequently add !
or ?
depending on the situation, which is
a good reminder to not assume that a piece is always where you expect, e.g.
// flip the top Card of the deck, if there are any
$.deck.first(Card)?.showToAll();
For convenience, all uniquely named spaces are also accessible from a global $
object that contains all spaces by name, e.g. $.deck
. These will be typed as
Space
and may need to be cast to more specific types.
There are many more methods and options for finding particular game elements. See the API documentation for more.
Creation
Spaces and pieces are created using the create method. All Game Elements have a class and a name. The Class can be one of the based classes or one of the subclasses you've declared. The name can be any string. It is used for searches, determining uniqueness, and also appears in the HTML for CSS targetting. e.g.:
const tableau = game.create(Space, "tableau");
const hand = tableau.create(Space, "hand");
hand.create(Card, "2C");
You can also specify the properties during their creation with a 3rd argument:
hand.create(Card, '2C', { suit: 'C', number: 2 });
hand.create(Card, 'JS', { suit: 'S', number: 1 1});
Ownership
All Game Elements also have an optional player
property built-in. Setting this
property assigns the element to a Player. This is
useful for pieces and spaces that permanently belong to them, like their player
mat, or their unique player token. These elements can be created and queried
using the player
property.
// create 2 tableaus for each player
game.create(Space, "tableau", { player: game.players[0] });
game.create(Space, "tableau", { player: game.players[1] });
// get player 1's tableau
game.first(Space, "tableau", { player: game.players[0] });
Any elements that are contained within an element assigned to a player are also
considered to be "owned" by that player, e.g. a card in their hand. These
elements can be queried using the owner
property.
// get player 1's cards
game.all(Card { owner: game.players[0] });
Remember the difference between player
and owner
. They are related but distinct.
player
is a property that can be set that assigns a game element to that player. It does not change automatically as a piece moves, so must be set again if you wish it to be assigned to a new player.owner
is a read-only property that indicates if the piece currently resides in a space assigned to a player. As the piece moves this property is automatically updated to indicate who the current owner is. A Card might be owned by a player while they hold it, but if it moved to another player's hand, then theowner
would be updated to reflect that change.
The Player object also conveniently has methods for retrieving these elements: my and allMy for retrieving one or many elements respectively
// get player 1's tableau
game.players[0].my("tableau")!;
// get player 1's cards
game.players[0].allMy(Card);
Visibility
All elements are visible to all players by default. Often a game will require
that pieces are visible only to some players and hidden from others. In
Boardzilla, "hiding" a Game Element means that the properties of that element
are no longer visible. For example if one of our example Card
instances was
flipped over, the player would be able to see only that it was an instance of
the Card
class, but it's properties, like name
, suit
and number
or any
others would be undefined
.
This can be accomplished in a number of ways, the simplest being
hideFromAll
. There are many other
methods for managing visibilty. It may
also be that some properties should be visible even when the element is hidden,
e.g. a Card may belong to different decks, and this is represented by
Card#deck
. If the card back art indicates what deck it belongs to, then this
property should be revealed even if the card is hidden. You can set these
properties with the static method
revealWhenHidden
, e.g.:
Card.revealWhenHidden("deck");
Players can also have invisible properties, such as hidden roles. To make a
property of your Player class invisible to other players, simple call the static
method Player#hide
. This property will be
undefined on other players when seen from a specific player's perspective
(i.e. in an action choice or in UI code).
class MyPlayer extends Player<MyGame, MyPlayer> {
secretRole: 'normie' | 'killer';
}
MyPlayer.hide('secretRole');
Movement
Pieces can be created in any Space or on the game itself. They can then move
around as players take their actions. There are several ways to do this but the
simplest is putInto
.
// discard a card
card.putInto($.discard);
// draw the top card of the deck into the field
$.deck.first(Card).putInto($.field);
As Pieces move from space to space, you may want to change their
properties. These can be done automatically by adding event handlers to
spaces. The most common type is to have spaces that change the visibility of
their elements. E.g. when a card enters the deck, it should automatically be
turned face down, or, when it enters a player's hand, it should be visible only
to that player. This can be done with the
onEnter
event handler. For example:
// the deck's cards are always face down
$.deck.onEnter(Card, card => card.hideFromAll();
// the player's hand always reveals their cards to `player`
const hand = game.create(Space, 'hand', { player });
hand.onEnter(Card, card => card.showTo(player));
There is also a corresponding onExit
handler.
The pile and removing pieces
There is a special invisible region of the game called the "pile" available at
game.pile
. This is the holding area for any
pieces that are not in use. The pile is never rendered, but is always available
to the API for querying. Pieces are never created or destroyed once the game has
started, and instead are simply moved to or retrieved from the pile.
Remove a piece (move it to the pile) simply by calling its
remove
method, or for a list of items,
the ElementCollection#remove can be
used. For example, to remove all cards from the deck that are lower than 5, we
can say:
$.deck.all(Card, (card) => card.number < 5).remove();
to put all the unused cards from the pile into the deck, we would say:
game.pile.all(Card).putInto($.deck);