Implementing the Ludo Die in the Playground
TL;DR
We illustrate how we can implement a Die for the Ludo game working from the Playground, and using it to document our steps.
Creating the Die class and roll method
First we want amodel of a Die. When we roll it we get a random number from 1 to 6.
How to get a random number:
(1 to: 6) atRandom
We want to encapsulate this as a class. We first create the class as a fixit, and then add the method:
GtLudoDie new roll
(To see how this works, slightly modify the name
GtLudoDie
— you will see a "fixit" wrench icon prompting you to create the class.)
We want to be able to see the last value rolled, so we should store it. We'll update the roll method to save the state in a topFace
slot.
GtLudoDie>>#roll
Again we use a fixit to create the missing slot.
If we inspect the Raw view of an instance at this point we see the slot is there, but it is initially nil.
GtLudoDie new
We should initialize a new die by initially rolling it. We add the GtLudoDie>>#initialize
method using the Meta
view.
If we inspect it again, the topFace
slot is now initialized.
Let's add a getter called topFace
.
Later we will make the Die observable by adding an announcer, but first let's create a nice view.
Prototyping a view
We'd like a nice visual display of the last rolled value of the Die.
Instead of directly implementing a new GtLudoDieElement class, let's first prototype it in the Playground.
Luckily we remember the Memory game, so we can first have a look at the GtMemoryGameExamples
to see how the cards are implemented.
We first create a square BlElement with rounded corners and a light ivory color. We'll just hard-code the paramters for now, and later move them to methods.
face := (BlElement new) size: 100 @ 100; background: Color paleBuff; border: (BlBorder paint: Color veryVeryLightGray width: 1); geometry: (BlRoundedRectangleGeometry cornerRadius: 12); yourself
We want to put black dots on the face. We assume a 3x3 grid for placing the dots.
Let's suppose each dot has a diameter d
, and the space between dots and the sides or other dots is s
. Then the total width of the die is 3d+4s
.
If we take s=10
and d=20
, then we have die sides of length 100
.
We find the circle example (BlBasicExamples>>#circle
) and adapt it.
dot := (BlElement new) geometry: BlCircleGeometry new; size: 20 @ 20; background: Color black; yourself
Since we'll need multiple dots (and we don't have a class yet to park a factory method), we'll create a factory block for dots:
newDot := [ (BlElement new) geometry: BlCircleGeometry new; size: 20 @ 20; background: Color black; yourself ]. newDot value
While we're at it, we'll make a factory block for the face:
newFace := [ (BlElement new) size: 100 @ 100; background: Color paleBuff; border: (BlBorder paint: Color veryVeryLightGray width: 1); geometry: (BlRoundedRectangleGeometry cornerRadius: 12); yourself]. newFace value
Let's test placing a single dot in the middle of the face:
dotSpace := 10. dotWidth := 20. centerStart := dotWidth + (2 * dotSpace). (newFace value) addChild: (newDot value relocate: centerStart @ centerStart); yourself
Not bad. Let's try to turn this into a class.
Creating the view class
We'll create a view class as a subclass of BlElement
in the same package, using the UI
tag:
GtLudoDieElement new
We'll use the Meta tab to define GtLudoDieElement>>#initialize
.
Now's a good time to define the constant methods GtLudoDieElement>>#dotWidth
, GtLudoDieElement>>#dotSpace
, and compute GtLudoDieElement>>#dieWidth
from them.
And now we can define the pixel location of the dots from their logical 3x3 position.
GtLudoDieElement new placeDotAt: 1@1
Looks ok. Let's create the higher-level interface to show the face for a particular rolled value.
We'll need to hard-code the positions of the dots for each face value in a collection.
GtLudoDieElement new showFace: 3
Let's see them all!
container := BrVerticalPane new hMatchParent; vFitContent. container addChildren: ((1 to: 6) collect: [ :each | GtLudoDieElement new showFace: each ]). container
Adding the view to the Die class
We'll want to get an element from the model instance, so we should implement asElement
. We'll have to create a new GtLudoDieElement
and save the die as a slot.
Now we can inspect the element of a die and immediately see its view.
GtLudoDie new asElement
However what we really want is to add this view to the inspector for the die itself.
Let's see how other similar views are implemented. We look for methods with the gtView
pragma that also send asElement
, and we sort them by size so we can see the small methods.
(#gtView gtPragmas & #asElement gtSenders) contents sort: [ :a :b | a linesOfCode < b linesOfCode ]
Now we can implement GtLudoDie>>#gtLiveFor:
in an easy way, and we can directly get the Live view when inspecting a die.
GtLudoDie new
Unfortunately, if we evaluate self roll
in the playground of the inspector instance, nothing happens because the view is not automatically updated.
(Don't forget, we are looking at the final, complete implementation, so the automatic updates have already been implemented, but from the perspective of this tutorial, we haven't done that yet!)
Updating the view
To automatically update the view, the die must announce to its subscribers (i.e., the view) that it has been updated.
We need to (1) add an announcer to the die, (2) on an update, announce a new type of update announcement to subscribers, (3) subscribe the view to the die's announcer, (4) update the view when the announcement is made.
First we add the announcer to the initialize method: GtLudoDie>>#initialize
We announce updates in GtLudoDie>>#roll
.
Next we patch the view to subscribe to announcements, in GtLudoDieElement>>#die:
With some luck (and maybe a bit of debugging) we can now inspect a new die instance, and see the changes as the die is rolled.
Inpect this:
die := GtLudoDie new
and evaluate (don't inspect) this:
die roll
Clicking to roll the die
We want to roll the die when we (double) click on its view.
We search in Spotter for “ClickEvent” and find BlDoubleClickEvent
. If we browse the Announcement references
view, we find the #when:do:
method. For example, we see that BrEditableLabel>>#initialize
registers for double click events.
We subscribe to double clicks in the same way in GtLudoDieElement>>#die:
.
Now the die is rolled when we double-click on it.
GtLudoDie new