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 roll topFace := (1 to: 6) atRandom. self notifyRolled. ^ topFace

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 initialize announcer := Announcer new. self roll 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 Object subclass: #GtMemoryGameExamples instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Demo-Memory-Examples' 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 circle <gtExample> ^ BlElement new geometry: BlCircleGeometry new; background: (Color red alpha: 0.8); border: (BlBorder paint: (Color blue alpha: 0.5) width: 6); yourself ) 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 Object subclass: #BlElement uses: TBlTransformable + TBlEventTarget + TBlDebug instanceVariableNames: 'spaceReference parent children bounds measuredBounds boundsCache eventDispatcher constraints layout transformation taskQueue errorHandler userData visuals flags' classVariableNames: '' package: 'Bloc-Basic' in the same package, using the UI tag:

GtLudoDieElement new 
  

We'll use the Meta tab to define GtLudoDieElement>>#initialize initialize super initialize. self size: self dieWidth @ self dieWidth; background: Color paleBuff; border: (BlBorder paint: Color veryVeryLightGray width: 1); geometry: (BlRoundedRectangleGeometry cornerRadius: 12) .

Now's a good time to define the constant methods GtLudoDieElement>>#dotWidth dotWidth ^ 20 , GtLudoDieElement>>#dotSpace dotSpace "Space between a dot and the edge of the die or another dot" ^ 10 , and compute GtLudoDieElement>>#dieWidth dieWidth "There is space for three dots plus spaces between and around them." ^ 3 * self dotWidth + (4 * self dotSpace) 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 BlElement subclass: #GtLudoDieElement instanceVariableNames: 'die' classVariableNames: '' package: 'GToolkit-Demo-Ludo-UI' 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: gtLiveFor: aView <gtView> ^ self asElement gtLiveFor: aView 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 initialize announcer := Announcer new. self roll

We announce updates in GtLudoDie>>#roll roll topFace := (1 to: 6) atRandom. self notifyRolled. ^ topFace .

Next we patch the view to subscribe to announcements, in GtLudoDieElement>>#die: die: aDie die := aDie. self initializeAnnouncements

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 BlMouseEvent subclass: #BlDoubleClickEvent instanceVariableNames: '' classVariableNames: '' package: 'Bloc-Events' . If we browse the Announcement references view, we find the #when:do: method. For example, we see that BrEditableLabel>>#initialize initialize super initialize. labelShortcuts := self defaultLabelShortcuts. editorShortcuts := self defaultEditorShortcuts. self editor: BrEditableLabelModel new. self fitContent. self when: BlDoubleClickEvent do: [ :anEvent | anEvent consumed: true. anEvent currentTarget switchToEditor ]. self when: BlClickEvent do: [ :anEvent | anEvent consumed: true. anEvent currentTarget requestFocus ]. self when: BlBlurEvent do: [ :anEvent | anEvent isDueToRemoval ifFalse: [ anEvent currentTarget acceptEdition ] ]. self switchToLabel registers for double click events.

We subscribe to double clicks in the same way in GtLudoDieElement>>#die: die: aDie die := aDie. self initializeAnnouncements .

Now the die is rolled when we double-click on it.

GtLudoDie new