Most games beyond very simple ones tend to have different behaviors at various phases. A game might have a phase at the beginning where players are drafting cards before entering a playing phase, for example.
Each phase in boardgame.io defines a set of game configuration options that are applied for the duration of that phase. This includes the ability to define a different set of moves, use a different turn order etc. Turns happen inside phases.
Let us start with a contrived example of a game that has exactly two moves:
function DrawCard({ G, playerID }) {
G.deck--;
G.hand[playerID]++;
}
function PlayCard({ G, playerID }) {
G.deck++;
G.hand[playerID]--;
}
const game = {
setup: ({ ctx }) => ({ deck: 6, hand: Array(ctx.numPlayers).fill(0) }),
moves: { DrawCard, PlayCard },
turn: { minMoves: 1, maxMoves: 1 },
};
?> Notice how we moved the moves out into standalone functions instead of inlining them in the game object.
We’ll ignore the rendering part of this game, but this is how it might look. Note that you can draw or play a card at any time, including taking a card when the deck is empty.
<iframe class='plain' src='snippets/phases-1' height='350' scrolling='no' title='example' frameborder='no' allowtransparency='true' allowfullscreen='true'></iframe>
Now let’s say we want the game to work in two phases:
In order to do this, we define two phases. Each phase can specify its own
list of moves, which come into effect during that phase:
const game = {
setup: ({ ctx }) => ({ deck: 6, hand: Array(ctx.numPlayers).fill(0) }),
turn: { minMoves: 1, maxMoves: 1 },
phases: {
draw: {
moves: { DrawCard },
},
play: {
moves: { PlayCard },
},
},
};
!> A phase that doesn’t specify any moves just uses moves from
the main moves section in the game. However, if it does,
then the moves section in the phase overrides the global
one.
The game doesn’t begin in any of these phases. In order to begin
in the “draw” phase, we add a start: true to its config. Only
one phase can have start: true.
phases: {
draw: {
moves: { DrawCard },
+ start: true,
},
play: {
moves: { PlayCard },
},
}
Let’s also end the “draw” phase automatically once the deck is empty.
phases: {
draw: {
moves: { DrawCard },
+ endIf: ({ G }) => (G.deck <= 0),
+ next: 'play',
start: true,
},
play: {
moves: { PlayCard },
},
}
endIf ends the phase that it is defined in when it returns
true. The game is returned to a state where no phase is
active. However, for this game, we want to move to
the “play” phase once the “draw” phase is done. We specify a
next option for this, which tells the framework to go to that
phase.
Watch our game in action (now with phases). Notice that you can only draw cards in the first phase, and you can only play cards in the second phase.
<iframe class='plain' src='snippets/phases-2' height='350' scrolling='no' title='example' frameborder='no' allowtransparency='true' allowfullscreen='true'></iframe>
You can also run code automatically at the beginning or end of a phase. These are specified just like normal moves in onBegin and onEnd.
phases: {
phaseA: {
onBegin: ({ G, ctx }) => { ... },
onEnd: ({ G, ctx }) => { ... },
},
};
?> Hooks like onBegin and onEnd are run only on the server in
multiplayer games. Moves, on the other hand, run on both client
and server. They are run on the client in order to facilitate
a lag-free experience, and are run on the server to calculate the
authoritative game state.
The game and phase endIf conditions are checked after each step in the
game lifecycle. After a move or a change to active stages, the turn endIf
condition is checked as well. These checks happen in order from the broadest
scope to the narrowest:
game.endIf? → phase.endIf? → turn.endIf?
If a condition returns a truthy value, the corresponding game, phase, or turn
ends and the remaining checks in that sequence are skipped. The examples below
show the order when all endIf conditions return false. A question mark means
that a condition is evaluated.
When the game starts in phase A, the phase begins before the first turn order is initialised:
game.endIf?
phaseA.endIf?
phaseA.onBegin
game.endIf?
phaseA.endIf?
turn.order.playOrder
turn.order.first
turn.onBegin
game.endIf?
phaseA.endIf?
After a move, the turn’s onMove hook runs before end conditions are checked:
move
turn.onMove
game.endIf?
phase.endIf?
turn.endIf?
When a turn ends, the next player is selected before the new turn begins:
turn.onEnd
game.endIf?
phase.endIf?
turn.order.next
game.endIf?
phase.endIf?
turn.onBegin
game.endIf?
phase.endIf?
When phase A ends and its next option selects phase B, the current turn and
phase end before phase B and its first turn begin:
turn.onEnd
phaseA.onEnd
game.endIf?
select phase B
game.endIf?
phaseB.endIf?
phaseB.onBegin
game.endIf?
phaseB.endIf?
turn.order.playOrder
turn.order.first
turn.onBegin
game.endIf?
phaseB.endIf?
Hooks can also call game events, although not every event is supported in every hook. See Calling events from hooks for the supported combinations.
The two primary ways of moving between phases are by calling the following events:
endPhase: This ends the current phase and returns the game
to a state where no phase is active. If the phase specifies a
next option, then the game will move into that phase instead.
setPhase: This ends the current phase and moves the game into
the phase specified by the argument.
endIf conditionYou can also end a phase by returning a truthy value from its
endIf method:
phases: {
phaseA: {
next: 'phaseB',
endIf: ({ G, ctx }) => true,
},
phaseB: { ... },
},
!> Whenever a phase ends, the current player’s turn is first ended automatically.
Instead of setting a phase’s next option with a string, you can
provide a function that will return the next phase based on game
state at the end of the phase:
phases: {
phaseA: {
next: ({ G }) => {
return G.condition ? 'phaseC' : 'phaseB';
},
},
phaseB: { ... },
phaseC: { ... },
},
As observed above, a phase can specify its own moves section
which comes into effect when the phase is active. This moves
section completely replaces the global moves section
for the duration of the phase. The moves may have the
same name as their global equivalents, but they are not related
to them in any way.
A phase can similarly also override the turn section. You will
typically do this if you want to use a different
Turn Order during the phase.