In het eerste blog van deze serie, hebben we het datamodel ontworpen voor ons Monopoly spel. Gaandeweg de verdere ontwikkeling van het spel zullen we regelmatig inzoomen op het datamodel en velden toevoegen wanneer we ze nodig hebben.
Nu het bord gemaakt is, wil ik uitwerken hoe spelers zich over het bord gaan bewegen.
Elke speler start op het vakje Start, dat zich op plek 0 van het bord bevindt. Na de worp gaat de speler naar het vakje met de index index van startpositie van de beurt + waarde van de worp
. Er zijn daarvoor verschillende deelproblemen die we moeten aanpakken.
- Hoe gooi je de dobbelstenen?
- Hoe ga je na een hele ronde voorbij start?
- Wat moet er gebeuren bij de afhandeling van een worp?
Het gooien van de dobbelstenen
We gaan voor de meeste handelingen in het spel flows gebruiken. Voor het gooien van de dobbelstenen zullen we een Apex Action nodig hebben, want willekeurige getallen genereren is iets dat Flow niet zelf kan.
Omdat je voor Apex oplossingen altijd een echte developer moet inschakelen, gaan we niet in op de Apex code zelf. Ik leg wel uit hoe we deze Action gebruiken. De input die je aan de Action moet meegeven zijn twee hele getallen:
- Het aantal zijden op elke dobbelsteen
- Het aantal dobbelstenen dat gegooid wordt
De output wordt een verzameling van hele getallen. De code genereert 2 getallen tussen de 1 en de 6.
Waarom niet gewoon 1 getal tussen de 2 en de 12 genereren?
- Omdat de kansverdeling niet gelijk is. Wanneer je met 2 dobbelstenen gooit, zijn er 6 verschillende mogelijkheden om 7 te gooien, terwijl de uitkomst 2 of 12 maar bij een combinatie mogelijk is.
- De tweede reden is dat dubbel gooien een functie heeft in het spel, dus het is van belang de afzonderlijke waarden van de twee dobbelstenen te weten. Op dubbel gooien komen we later terug.
Hiervoor heb je deze apex class en de bijbehorende test class nodig.
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
Om deze Apex classes aan te maken ga je naar Setup > Apex Classes en daar klik je op de New knop.

Plak de code en klik op Save.
Aan de slag met de eerste flow
Na de worp gaat de speler zich verplaatsen. De nieuwe positie waar de speler naartoe moet, moet dus berekend worden. In de basis is het: Index van de positie waar de speler begint + totale waarde van de worp
.
Sta je op Start (vakje 0) en gooi je 9 dan kom je uit op vakje 9, Velperplein Arnhem. Maar sta je op Leidsche straat Amsterdam (vakje 36) en gooi je 5, dan moet je uitkomen op vakje 1, Dorpsstraat Ons Dorp in plaats van op vakje 41, wat niet bestaat.
De formule voor de Index van de nieuwe positie is dus:
{!recordId.Position__r.Index__c} + {!rollTotalValue} -IF( {!recordId.Position__r.Index__c} + {!rollTotalValue} > 39, 40, 0)
Het vakje met die Index moet worden opgehaald. Op het scherm tonen we aan de speler welk vakje de nieuwe positie is.
Ga naar Setup > Flows en klik op de New Flow knop.

Eerst maken we een Autolaunched Flow (No Trigger) waar we de Apex action in gebruiken en waarin we wat extra variabelen bepalen, zoals de totale waarde van de worp en of er dubbel gegooid is.
Het eerste element dat we in de flow zetten is een Action. We kunnen gewoon zoeken door te beginnen roll te typen.

Selecteer de diceRoller action. Deze heeft twee inputvariabelen. Het aantal dobbelstenen en het aantal zijden op de dobbelstenen. We willen twee 6-zijdige dobbelstenen gooien.

De output van deze Action is een verzameling van getallen. Om wat met de afzonderlijke getallen in die verzameling te kunnen doen, hebben we dus een Loop nodig.

Terwijl de Apex Action heel abstract en dus flexibel is, gebruik ik deze flow om de informatie uit de worp te halen die specifiek voor het Monopoly spel van belang is: de totale waarde van de twee dobbelstenen samen en of beide dobbelstenen dezelfde waarde hadden.
We gaan uiteindelijk eerst bepalen of er dubbel gegooid is en daarna de totale waarde bepalen, maar ik bouw het juist in de omgekeerde volgorde.
Om de totale waarde van de worp te bepalen en door te kunnen geven aan de screenflow maken we een variabele van het type Number:
- naam: rollTotalValue
- aantal decimalen: 0
- standaard waarde: 0
- available for output: aangevinkt

Om de totale waarde te bepalen, tellen we in de Loop de waarde van het huidige element op bij de waarde die rollTotalValue
al heeft.

Hierna wordt het gemakkelijker om te bepalen of de worp dubbel was. Nadat je de waarde van de eerste dobbelsteen bij rollTotalValue
hebt opgeteld, kun je de waarde van rollTotalValue
vergelijken met de waarde van de tweede dobbelsteen.

Zijn die twee waarden gelijk, dan zetten we de volgende variabele op true.
- naam: isDouble
- type: boolean
- standaard waarde: false
- available for output: aangevinkt

Deze beslissing en toewijzing moeten we vóór de andere toewijzing binnen de Loop plaatsen. De waarde van rollTotalValue
moet op het moment dat de je langs de beslissing komt nog alleen de waarde van de eerste dobbelsteen bevatten.

De subflow hoeft verder niets te doen, dus is die nu klaar.
De Screen Flow
Nu beginnen we aan onze screenflow voor het uitvoeren van de beurt van een Player. De flow zal waarschijnlijk gestart worden via een Action op de Player pagina of wellicht zal de flow zelfs rechtstreeks als component op de pagina staan.
Dus laten we onze input variabele recordId
noemen. De variabele met deze naam wordt in Actions en Lightning Pages automatisch gevuld.
De naam doet wellicht anders vermoeden, maar de recordId
variabele kan gewoon een record variabele zijn. Hierdoor hoef je niet zelf nog een query (Get Records element) in de flow op te nemen om de Player op te halen.

Het gooien van de dobbelstenen moet een bewuste actie zijn die niet automatisch kan worden getriggerd, dus de subflow wordt niet het eerste element van de flow. Eerst komt er een scherm.

Daarna plaatsen we de Subflow.

Daarna willen we de speler het resultaat van de worp laten zien en naar welke vakje de speler zich moet verplaatsen. We hebben de output variabalen van de subflow dus nodig en gaan de eerder beschreven formule maken om te berekenen wat de nieuwe positie is.

De output van deze formule gebruiken we om de Position op te zoeken waar de speler naartoe moet.

Sorteren op External Id doe ik bewust omdat er twee Positions zijn met Index 10: Slechts op Bezoek en In de Gevangenis. Door een gewone worp, kom je nooit IN de gevangenis. Die laatste heeft een hogere External Id waarde en zal door in de query te sorteren op die External ID nooit het resultaat zijn dat de query terug geeft.
Nu maken we een scherm om de speler te laten zien:
- Hoeveel ze hebben gegooid
- Naar welke Position ze moeten

Dit is een mooi moment om de flow te gaan testen.
- We slaan de flow op en klikken op Debug
- Controleer of je Player op een Position staat
- Selecteer de Player die je voor jezelf gemaakt hebt en klik op Run


Nadat de speler op de Move knop gedrukt heeft, updaten we de Position van de Player record. We updaten de waarde van de Position lookup van deze record.


Informatie over het nieuwe vakje en wat de speler daar mag en moet doen, zullen we in een later artikel toevoegen.
Dubbele worp
Waar we deze keer wel op ingaan is dubbel gooien. De regel is dat als je dubbel gooit, je nog een keer mag gooien, maar dat je bij de derde keer dubbel in dezelfde beurt juist naar de gevangenis moet.
In de flow zoals we die tot nu toe hebben gemaakt, moeten we dus een decision element toevoegen waarin we checken of de speler dubbel gegooid heeft. Zo ja, dan verbinden we de connector bij het positieve pad van de beslissing met het beginscherm om dezelfde stappen nogmaals uit te voeren.
Hiernaast moeten we een teller laten meelopen. Daarvoor maken we een nieuwe getal variabele met 0 cijfers achter de komma en standaard waarde 0: numberOfDoubleRolls. In het positieve pad van de decision tellen we daar 1 bij op.

We voegen een extra uitkomst aan de beslissing toe: isDouble
equals true and numberOfDoubleRolls
greater than 2. Die zetten we in volgorde op de eerste plek. Hierbij gaan we de Player Position updaten naar het vakje waar ik de speler in de gevangenis zit.
Omdat de informatie die we op het scherm aan de speler tonen verschillend is afhankelijk van of de speler dubbel gegooid heeft of niet, moet de beslissing dus vóór deze schermen zitten.

Het Get New Position element zit nog vóór de decision, omdat we het hierdoor maar één keer hoeven te plaatsen. Ik heb de beslissingen of er dubbel gegooid is en of er voor de derde keer dubbel gegooid is ingericht als twee afzonderlijke beslissingen. Ik vind dat zelf overzichtelijker staan en probeer beslissingen met meerdere aftakkingen altijd te vermijden. Voor iemand anders die later aan de flow werkt, kost het bij complexe decision elements vaak langer om te analyseren wanneer welke uitkomst nu precies geldt.
Je ziet na de vertakkingen dat steeds aan de speler informatie over de worp en nieuwe positie getoond wordt en de nieuwe positie alvast wordt toegewezen aan de recordId
(Player record) variabele.
Alleen wanneer de speler een derde keer dubbel gegooid heeft en naar de gevangenis moet, moeten we eerst even de Position record voor de gevangenis ophalen.
Je ziet dat ik ook twee afzonderlijke update elementen gebruik. Ik heb hier een afweging gemaakt. Als ik alles via één update element had laten lopen, zou ik daarna een tweede decision hebben moeten gebruiken om weer te splitsen voor de situatie waarbij de speler meteen nog een keer mag gooien. Omdat dezelfde beslissing eerder in de flow al zit, vind ik die oplossing minder elegant.
Nu zie je dat het pad voor de eerste of tweede dubbele worp een eigen update element heeft en daarna terug gaat naar het eerste scherm om de gehele flow nogmaals te doorlopen voor een tweede of derde worp.
Hier zie je hoe de flow er voor de speler uitziet wanneer deze dubbel gooit en daarna een ‘gewone’ worp heeft. Je ziet ook dat bij de tweede worp verder gelopen wordt vanaf de Position waar de speler bij de eerste dubbele worp naartoe gegaan is.

Hiermee hebben we nu de basis staan voor het verplaatsen over het bord.
In de volgende editie zullen we ons bezig houden met wat er moet gebeuren wanneer de speler op een nieuwe Position arriveert.