Skip to main content

Flow

The Flow of your game is how the game runs from beginning to end. This describes the phases, rounds and turns of the game, and what actions are available to players at various points in the Flow. You describe the Flow using the available API and Boardzilla keeps track of where your players are in the Flow.

Your Flow definition will contain at minimum some player actions, and usually some loops around them with logic to decide when the game is over.

Typical flow structure

Flow is defined in game/index.ts inside the createGame when you call game.defineFlow. The arguments to this function are the steps for your game. There are two basic types of steps:

  • Functions
  • Flow commands

Functions alter the game state in some way, and Flow commands change the flow of the game. You can keep adding as many functions and Flow commands as needed, in any order. Let's look at a simple example from the Boardzilla starter game:

  game.defineFlow(
() => $.pool.shuffle(),
loop(
eachPlayer({
name: "player",
do: playerActions({
actions: ["take"],
}),
})
)
);

This flow contains exactly two items: a function call that shuffles the pool, and the main play loop. The first function makes a single change:

  () => $.pool.shuffle(),

Generally the first function of a flow will set up the board, shuffling decks, dealing out opening hands and the like. Later in the flow you can use functions as much as you like to make other changes to the board state, including moving pieces around that are not part of a player action (e.g. dealing out a new hand) or just changing state on the board or players (e.g. updating the score).

Flow may not create or destroy game elements

One thing you cannot do in the flow is create new elements. All Spaces and Pieces in your game must be created before calling defineFlow. If pieces are only needed later in the game, they can be created on the pile using game.pile.create() and moved from the pile when needed. Similarly when elements are removed they actually are put into the pile.

The next argument in this example calls loop. This is a basic Flow command that causes a portion of the flow to be repeated until something interrupts it. In this case, it sets up the basic game loop. Flow commands can set up other types of loops, have players take turns, create branching logic, and most importantly, prompt players for the actions they can perform.

In the example above the main loop itself includes other Flow commands, namely eachPlayer and playerActions. This is an important distinction between the two types of flow steps, plain functions and Flow commands:

Flow commands must be separated

Each Flow command is its own "step" in the flow. You cannot include flow commands inside functions.

For example, imagine you have a flow function that does some things, setting up the deck, dealing some cards, and then cleaning up after the hand:

  game.defineFlow(
() => {
// shuffle the deck
// deal cards

// put the cards back in the deck
})
);

If you now want to insert a Flow command in the middle to add a player action, you cannot just add it to the function, e.g.:

  game.defineFlow(
() => {
// shuffle the deck
// deal cards

playerAction({ actions: ['playCard'] }); // ❌ wrong

// put the cards back in the deck
})
);

you must break the function into two pieces and insert the flow function between them, e.g.:

  game.defineFlow(
() => {
// shuffle the deck
// deal cards
},

playerAction({ actions: ['playCard'] }), // ✅ correct

() => {
// put the cards back in the deck
})
);
flow visualization

Getting the Flow right is critical, but it can be hard to conceptualize and visualize game flow in a formalized way like this.

As an aid to understanding the flow, you can use the debug mode of the devtools (the magnifying glass icon in the top left) to open up a visualization of the flow showing where the game is currently in the flow at any point.

Since the Flow is evaluated only once, what you see is what you get. If the flow is missing something, it is likely defined in the wrong place.

Flow arguments

Each flow function modifies the game state in some way. But how do you check the current state? The board state can simply be read by using the methods and properties on game, either ones that you've defined or the query functions, e.g. game.first('deck')!.all(Card).length. Using these you can access all the properties of the game and all the game elements within.

You can also access the flow arguments in any flow function. The arguments provide the current values for the parts of the flow that are currently being evaluated. There are two main types of flow arguments:

  • loop variables
  • player actions

While the flow is executing steps inside of loops and player actions, these values are provided to the each of these steps using the flow arguments, e.g.

  forEach({
name: "card",
collection: () => $.field.all(Card),
do: playerAction({
name: "choose",
actions: [
{
name: "chooseCard",
do: forLoop({
name: "tokens",
initial: 1,
next: tokens => tokens + 1,
while: tokens => tokens < 5
do: ({ card, choose, tokens }) => {
// here we have access to 3 values from the 3 steps we're inside of:
// `card` will be the card being looped over in the list of Card's on the field
// `choose` will be the choices the player made when they made the `chooseCard` action
// `tokens` will be the value of the tokens loop, from 1 to 4.
}
})
},
"pass"
]
}),
});

These arguments are also available for several of the properties in new flow steps. As just one example, the playerAction has a player property that can be either a Player or a function that returns a Player. We can use the function form to access the flow arguments, if we need to check the current flow to decide who the starting player is, e.g.:

  forEach({
name: 'player',
collection: () => [game.playerWithHighestScore(), game.playerWithMostCities()],
do: playerActions({
// Here 2 players get to vote, first the one with high score, then the player
// with the most cities. We've looped over the 2 players and then check the
// current player in the loop with the following:
player: ({ player }) => player,
actions: ['vote', 'pass']
})
}),

Flow commands

All Flow commands are available on game.flowCommands. It is common to deconstruct all needed commands before defining flow, e.g.:

const { playerActions, eachPlayer, forEach, forLoop } = game.flowCommands;

Let's look at the various Flow commands. There are 3 main types of flow commands, looping, branching and actions.

Looping Flow commands

loop

The most basic loop, this creates a loop that continues indefinitely until explicitly interrupted. This would be like a C/Javascript while(true).

whileLoop

Like the basic loop, except that it accepts a condition and will only start a new iteration of the loop if the condition is true. This is exaclty like the C/Javascript while(condition). In particular, note that this loop might not execute even one iteration if the supplied condition is false to begin with.

forLoop

This loop sets a value, iterates that value at each loop iteration and continues looping until that value meets some condition. In other words, a standard for loop from C/Javascript.

forEach

This loop accepts a collection, and iterates over the members of that collection. This is like for ... of or Array#forEach in Javascript.

eachPlayer

This is a loop that iterates over each player. This is the same as forEach with one addition. On each iteration as the player changes, it also automatically sets the "current" player.

everyPlayer

Strictly speaking, this isn't a loop. However, it looks identical to eachPlayer except that instead of operating on each player in turn, it let's all players take their turn in parallel. This "loop" completes when all players have completed the body of this command, or is otherwise interrupted.

Branching Flow commands

ifElse

Simply checks a condition and either takes one branch named do or an optional else branch, just as an standard if...else

switchCase

Like ifElse except the test expression can be compared with several possible values and execute different branches depending on the outcome. It may also execute a default branch if no other matches apply. This is similar to the switch...case in C/Javascript but without fall through behaviour.

Player Actions

playerActions

This is the sole Flow command for prompting player actions. This command accept a list of allowed actions that were defined in defineActions and prompts the current player (or a particular player or players if specified).

Actions can be supplied as strings, corresponging to the names given in defineActions e.g.:

actions: ["take", "pass"]

or as objects if additional properties are needed, e.g.:

actions: [
{
name: "take",
do: Do.repeat,
},
'pass'
]

The properties available are:

  • name: the name of the action
  • do: a continuation for the flow if this action is taken. This can contain any number of nested Flow functions.
  • args: args to pass to the action. If provided this pre-selects arguments to the action that the player does not select themselves, in the same way as Follow up's.
  • prompt: a string prompt. If provided this overrides the prompt defined in the action. This can be useful if the same action should prompt differently at different points in the game

Note that like all other selections in Boardzilla, this list of actions has tree-shaking and skipping behavior. If one of the included actions is determined to have no possible valid moves, it will not be included in player prompts. If only one of the supplied actions is determined to be playable, it will be prompted with any required selections. If such an action requires no further selections it will be auto-played. Just like action selections this behavior can be configured for each playerActions with a skipIf parameter.

For this reason, it is common to include a wide variety of possible actions in the list of playerActions but let each action definition take responsibility for determining whether it is actually playable at the time based on its selections and/or condition parameter.

The Player Actions step in the flow can be further customized with a few optional paramters:

  • prompt: A prompting message for the player to decide between multiple actions that involve clicking on the board.
  • description: A description of this step from a 3rd person perspective, used to inform other players as to what the current player is doing.
  • player: Which player can perform this action, if someone other than the current player
  • players: Same as above, for multiple players that can act.
  • optional: Make this action optional with a "Pass"
  • condition: Skip this action completely if condition fails
  • skipIf: One of 'always', 'never' or 'only-one' (Default 'always'). See Skipping.
  • repeatUntil: Make this action repeatable until the player passes
  • continueIfImpossible: Skip this action completely if none of the actions are possible.
Flow commands are created once

Unlike Actions that are created for each player at the time of being played, Flow commands are created at the beginning of the game. Be careful with passing expressions directly to Flow commands that rely on game state.

For example if you want to loop through some cards laid out in a Space called "field", something like the following is probably not what you want:

  forEach({
name: "card",
collection: $.field.all(Card), // ❌ only evaluated at the start of the game
do: playerAction({ actions: ["chooseCard", "pass"] }),
});

Instead use the functional form, so that the expression will be evaluated each time this loop is entered:

  forEach({
name: "card",
collection: () => $.field.all(Card), // ✅
do: playerAction({ actions: ["chooseCard", "pass"] }),
});

Current Flow Position

For many Flow commands, it is necessary to know what the current position is. For example in a simple for i loop, we need to access i and have logic that depends on its current value.

All function parameters in Flow commands accept a single argument of type FlowArguments for this purpose. The argument is a single object that contains all the values "in scope" at this point in the flow. There are two types of values included here:

  • loop variables
  • player action selections

Loop variables

The loop variables included here are from any loops that the flow is currently inside of, namely the current iterator value in any forLoop's, the current collection member of any forEach loops, the evaluated test expression in any switchCase's.

The values are included as key value pairs where the key is the name parameter supplied for the Flow command.

  forLoop({
name: "x", // x is declared here
initial: 0,
next: x => x + 1,
while: x => x < 3,
do: forLoop({
name: "y", // y is declared here
initial: 0,
next: y => y + 1,
while: y => y < 2,
do: ({ x, y }) => {
// x is available here as the value of the outer loop
// and y will be the value of the inner loop
},
}),
});

Player action selections

For player action selections, the arguments to the player action are included as a single object. Again this only applies if the Flow command is inside the do branch belonging to this player action. The name in this case is the name of the actions. For example, here we have defined an action called "takeResource" and later we want to know what choices the player made in the flow.

  game.defineActions({
...
takeResources: player => action<{ amount: number }>({
prompt: "Take resources",
}).chooseFrom(
"resource", ["Lumber", "Steel", "Wheat"]
).do(
({ resource, amount }) => player.addResources(resource, amount)
),
...
});

game.defineFlow(
...
playerActions({
actions: [
{
name: "takeResources",
// The argument is the name of the action
do: ({ takeResources }) => {
// takeResoures.resource will be the name of the resource, e.g. 'Steel'
// takeResoures.amount will be the selection number "amount"
},
},
],
})
);
Action vs playerActions

We can react to a player's action both in the action do and in the do of the playerActions. It can be confusing which we should use for what.

In general the action do is the proper place to react to what a player just did. This includes mutating the board, recording state, or triggering follow-up actions.

The playerActions do should be used only for changes to the flow of the game as a result of the player action, e.g. ending a phase or somehow interrupting a loop, or triggering other rounds of player actions, since Flow commands can only be issued inside other Flow commands.

Loop interruption

It is important in a game to able to interrupt loops. In fact if we use the basic loop Flow command, the loop will continue indefinitely unless we interrupt it. There are 3 basic loop interruption functions:

Do.break()

This causes the flow to exit loop and resume after the loop, like the break keyword in C/Javascript.

Do.continue()

This causes the flow to skip the rest of the current loop iteration and restart the loop at the next iteration, like the continue keyword in C/Javascript.

Do.repeat()

Similar to Do.continue except this restarts the loop on the same iteration it's currently on.

These functions can be called anywhere that is called from a loop. Often it is the only thing you want to call after a particular action, in which case you can pass it as the action do, e.g.:

  loop(
playerActions({
actions: [
"takeOneFromBag",
{
name: "done",
// break out of the loop when a player selects 'Done'
do: Do.break,
},
],
})
);

All 3 flow interruption commands operate on the "current" loop, just as in C/Javascript. If you wish to operate on another loop higher up, you can pass an argument to the function with the name of the loop you wish to break out of, e.g.:

  ...,
whileLoop({
while: () => true,
name: 'outer-loop',
do: whileLoop({
while: () => true,
name: 'inner-loop',
// break here
do: () => Do.break('outer-loop')
}),
() => { /* will never reach here */ }
}),
() => { /* will resume here */ }

This operates much the same as a labelled statement in Javascript.

These are not javascript keywords

Remember that the flow interruption functions are merely humble Javascript functions, not keywords, despite being named similarly. They do not break control flow all by themselves.

  loop({
name: 'round',
do: () => {
if (game.isRoundFinished()) Do.break();
// otherwise do other stuff
game.doOtherStuff(); <-- will execute even if Do.break is called
}
});

Use e.g. return or else to control what executes, e.g.:

  loop({
name: "round",
do: () => {
if (game.isRoundFinished()) return Do.break();
// otherwise do other stuff
game.doOtherStuff();
},
});

Subflows

Every game defines its main flow by calling game.defineFlow. However, there can be more than one flow in a game. Additional flows are called "subflows" and are like subroutines in your game. Each subflow is given a unique name and at any point in the game, calling Do.subflow with the name of the subflow and any args you wish to pass to the subflow. Like with the loop interruption functions above, this does not throw or return from the current block and so the current block will evaluate any remaining statements. However, after doing so, game execution will jump to the new subflow and start at its beginning. When the subflow reaches the end, game execution will resume on the calling flow at the next step from the one that called the subflow.

The args passed to the subflow become the part of the flow arguments in this subflow. The subflow does not have access to any other flow arguments from the calling flow unless they passed in.

  game.defineFlow(
whileLoop({
while: () => game.all(Token).length > 0,
do: eachPlayer({
name: 'player',
do: [
playerActions({ actions: ['discardTokens']}),
// if 5 or fewer tokens remain, perform the `voting` subflow
// before the next round of token discarding
() => if (game.all(Token).length <= 5) Do.subflow('token-flow');
]
})
)}),
);

game.defineSubflow(
'voting',
eachPlayer({
name: 'player',
do: playerActions({
actions: ['vote']
})
})
);