In the first episode in this serieswe designed the data model for our Monopoly game. As we continue developing the game, we will regularly zoom in on the data model and add fields as needed.
Now that the board is complete, I want to outline how players will move across it.
Each Player starts at Go, which is located at position 0 on the board. After rolling the dice, the player moves to the square with the corresponding index. index of starting position + value of the dice rol
. There are several subproblems we need to address for this.
- How can you roll the dice?
- How do you reach or pass Go after a whole lap?
- What needs to happen when processing a dice roll?
Rolling the dice
For most actions in the game, we will use Flows. However, for rolling the dice, we will need an Apex Action, as generating random numbers is something Flow cannot handle on its own.
Since Apex solutions always require a professional developer, we won’t delve into the Apex code itself. However, I will explain how to use this Action. The input you need to provide to the Action consists of two integers:
- The number of sides on each die
- The number of dice being rolled
The output will be a collection of integers. The code generates two numbers between 1 and 6.
Why not just generate a single number between 2 and 12?
- Because the probability distribution is not uniform. When rolling two dice, there are six different combinations that result in a 7, while the outcomes of 2 or 12 are only possible with one specific combination each.
- The second reason is that rolling doubles has a specific function in the game, so it's important to know the individual values of the two dice. We'll address rolling doubles later.
For this, you will need the following Apex class and its corresponding test class.
cls_DiceRoller
public class cls_DiceRoller {
@InvocableMethod(
label = 'Roll Dice'
description = 'Returns the Dice roll\'s result as a list of Integers'
)
public static List<Response> roll( List<Request> requests ) {
List<Response> responses = new List<Response>();
for ( Request req : requests ) {
List<Integer> rolledResults = new List<Integer>();
for(integer i = req.numberOfDice; i > 0; i--) {
integer roll = integer.valueof(math.random() * req.numberOfSides) +1;
rolledResults.add(roll);
}
Response resp = new Response();
resp.rolledResults = rolledResults;
responses.add(resp);
} // End requests loop
return responses;
} // End converter method
public class Request {
@InvocableVariable(
label = 'Number of Dice'
required = true
)
public Integer numberOfDice;
@InvocableVariable(
label = 'Number of sides on a die'
required = true
)
public Integer numberOfSides;
} // End Request Class
public class Response {
@InvocableVariable(
label = 'Rolled Results'
)
public List<Integer> rolledResults;
} // End Response Class
} // End cls_DiceRoller
test_DiceRoller
@isTest
private class test_DiceRoller {
@isTest
static void myUnitTest() {
Test.startTest();
cls_DiceRoller.Request req = new cls_DiceRoller.Request();
req.numberOfDice = 2;
req.numberOfSides = 6;
cls_DiceRoller.Request[] requests = new cls_DiceRoller.Request[] {};
requests.add(req);
cls_DiceRoller.Response[] testResults = cls_DiceRoller.roll(requests);
List<Integer> results = testResults[0].rolledResults;
system.assertEquals(2,results.Size());
for(Integer r : results) {
system.assert.isTrue(6 >= r);
}
Test.stopTest();
}
} // End test_DateTimeToString
To create these Apex classes, navigate to: Setup > Apex Classes and click the New button's custom label.

Paste the code and click Save.
Getting started with the first Flow
After the dice roll, the player moves. The new position the player needs to go to must be calculated. The basic formula is: Index of the position where the player starts + total value of the roll
.
If you are on Start (square 0) and roll a 9, you land on square 9, Velperplein Arnhem. However, if you are on Leidsestraat Amsterdam (square 36) and roll a 5, you should end up on square 1, Dorpsstraat Ons Dorp, instead of square 41, which does not exist.
The formula for the index of the new position is:
{!recordId.Position__r.Index__c} + {!rollTotalValue} -IF( {!recordId.Position__r.Index__c} + {!rollTotalValue} > 39, 40, 0)
The square with that index must be retrieved. On the screen, we display to the player which square is the new position.
Go to Setup > Flows and click the New Flow button's custom label.

First we create a new Autolaunched Flow (No Trigger) where we use the Apex Action and define some additional variables, such as the total value of the roll and whether doubles were rolled.
The first element we add to the Flow is an Action. We can simply search by starting to type roll .

Select the diceRoller action. It has two input variables: the number of dice and the number of sides on the dice. We want to roll two 6-sided dice.

The output of this Action is a collection of numbers. To work with the individual numbers in that collection, we need a Loop.

While the Apex Action is very abstract and therefore flexible, I use this Flow to extract information from the roll that is specifically relevant to the Monopoly game: the total value of the two dice combined and whether both dice had the same value.
We will ultimately first determine whether doubles were rolled and then calculate the total value, but I am building it in the reverse order.
To determine the total value of the roll and pass it to the screen flow, we create a variable of type Number:
- name: rollTotalValue
- decimals: 0
- default value: 0
- available for output: checked

To calculate the total value, we add the value of the current element in the Loop to the value that rollTotalValue
already holds.

Afterward, it becomes easier to determine if the roll was a double. Once you have added the value of the first die to rollTotalValue
, you can compare the value of rollTotalValue
to that of the second die.

If the two values are equal, we set the following variable to true.
- name: isDouble
- type: boolean
- default value: false
- available for output: checked

This decision and assignment must be placed before the other assignment within the Loop. At the moment the decision is evaluated, the value of rollTotalValue
should still only contain the value of the first die.

This is all the subflow needs to do, so it is now complete.
The Screen Flow
Now we begin our screen flow for executing a Player's turn. The flow will likely be started through an Action on the Player page, or it may even be embedded directly as a component on the page.
So let's name out input variable recordId
. A variable with this name will be automatically populated in Actions and Lightning Pages.
The name might suggest otherwise, but the recordId
variable can also be a record variable. This means you don’t need to include a separate query (Get Records element) in the flow to retrieve the Player.

Rolling the dice must be a deliberate action that cannot be triggered automatically, so the subflow will not be the first element in the screen flow. A screen comes first.

After this element we add the subflow.

After that, we want to show the player the result of the roll and which square they need to move to. Therefore, we need the output variables from the subflow and will use the previously described formula to calculate the new position.

We use the output of this formula to look up the Position the player needs to move to.

I deliberately sort by External Id because there are two Positions with Index 10: Just Visiting and In Jail. Through a regular roll, you never land in jail. The latter has a higher External Id value and, by sorting the query by External Id, it will never be the result returned by the query.
Now we create a screen to show the player:
- How much they rolled
- To which position they should go

This is a good moment to test the flow.
- We will save the flow and click Debug
- Check if the Player you are using is on a Position .
- Select the Player you created for yourself and click Run


After the Player clicks the Move button, we update the Player record's Position.


Information about the new square and what the player may or must do there will be added in a later episode.
Rolled a double
What we will focus on this time is rolling doubles. The rule is that if you roll a double, you get to roll again, but if you roll a double a third time in the same turn, you must go directly to jail.
In the flow we have created so far, we need to add a decision element to check if the player has rolled double. If so, we connect the positive path of the decision to the starting screen in order to repeat the same steps again.
We also need to keep count of ho many times the player has rolled double. For this, we create a new number variable with 0 decimal places and a default value of 0: numberOfDoubleRolls. In the positive path of the decision, we increment this by 1.

We voegen een extra uitkomst aan de beslissing toe: isDouble
equals true and numberOfDoubleRolls
greater than 2. We place this first in the sequence. In doing so, we will update the Player Position to the Position where the player is in jail.
Because the information we display to the player on the screen differs depending on whether the player rolled a double or not, the decision must therefore come before these screens.

The Get New Position element is placed before the decision, as this allows us to include it only once. I have set up the decisions regarding whether a double has been rolled and whether it has been rolled for the third time as two separate decisions. Personally, I find this more organized, and I always try to avoid decisions with multiple branches. For someone else working on the flow later, it often takes longer to analyze exactly when each outcome applies in the case of complex decision elements.
You can see in each branch information about the roll and the new position is shown to the player, and the new position is assigned to the recordId
(Player record) variable.
When the player rolls a double for the third time and must go to jail, we first need to retrieve the Position record for the jail.
You can see that I also use two separate update elements. This was a deliberate choice. If I had routed everything through a single update element, I would have needed a second decision afterward to split again for the scenario where the player gets to roll again. Since the same decision already appears earlier in the flow, I find that solution less elegant.
Now you can see that the path for the first or second double roll has its own update element and then loops back to the first screen to go through the entire flow again for a second or third roll.
Here you can see what the flow looks like for the player when they roll a double and then follow it with a 'normal' roll. You can also see that the second roll continues from the Position the player reached after the first double roll.

With this, we now have the foundation in place for moving across the board.
In the next episode, we will focus on what needs to happen when the player arrives at a new Position.