Playing the Memory Game

The Game Element

In the previous section, we implemented the card element that is able to display back- and face- card sides, to flip, and to disappear. In this section, we implement the board with 4x4 cards as we defined previously in Game>>#gridSize.

To this purpose, we will define GameElement:

BlElement subclass: #GameElement
	instanceVariableNames: 'game'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Tutorial-MemoryGame-UI'.

GameElement class
	instanceVariableNames: ''
  

We then define accessors and initialize methods, defining a widget layout, background color and size:

"protocol: #accessing"

GameElement >> game
	^ game

"protocol: #accessing"

GameElement >> game: aGame
	game := aGame.
	self onUpdated

"protocol: #initialize"

GameElement >> initialize
	super initialize.
	self layout: BlGridLayout horizontal.
	self layout cellSpacing: 7.
	self background: Color gray.
	self constraintsDo: [ :c |
		c horizontal fitContent.
		c vertical fitContent ].

"protocol: #hooks"

GameElement >> onUpdated
	self game ifNil: [ ^ self ].
	self removeChildren.
	self layout columnCount: self game gridSize.
	self game availableCards do: [ :aCard | 
		| aCardElement |
		aCardElement := self newCardElement card: aCard.	
		self addChild: aCardElement ]
  

We need to implement the GameElement>>#newCardElement:

"protocol: #'instance creation'"

GameElement >> newCardElement
	^ CardElement new
  

GameElement>>#initialize defines the board visual aspects, including BlGridLayout BlLayout subclass: #BlGridLayout instanceVariableNames: 'impl cellSpacing orientation alignment' classVariableNames: '' poolDictionaries: 'BlGridLayoutConstants' package: 'Bloc-Layout-Grid' , background color, and constraints to occupy the minimum horizontal and vertical size to display all cards:

GameElement>>#game: set the game model Game and call an update hook.

The GameElement>>#onUpdated method then defines BlGridLayout BlLayout subclass: #BlGridLayout instanceVariableNames: 'impl cellSpacing orientation alignment' classVariableNames: '' poolDictionaries: 'BlGridLayoutConstants' package: 'Bloc-Layout-Grid' layout dimensions calling BlGridLayout>>#columnCount: columnCount: aNumber impl columnCount: aNumber and adds all CardElement instances representing cards.

You can check the current implementation executing the following snippet:

aGame := Game numbers.
GameElement new game: aGame
  

Handling Mouse Clicks

If you try to click on cards above, you will notice that they do not yet flip as is expected in this game. The reason is that we have not implemented mouse click events yet. To handle the mouse click events, we will subclass BlEventListener BlBasicEventHandler subclass: #BlEventListener instanceVariableNames: '' classVariableNames: '' package: 'Bloc-Events-Handler' :

BlEventListener subclass: #CardEventListener
	instanceVariableNames: 'game'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Tutorial-MemoryGame-UI'.

CardEventListener class
	instanceVariableNames: ''
  

We then implement accessors and the mouse click event handler:

"protocol: #accessing"

CardEventListener >> game
	^ game

"protocol: #accessing"

CardEventListener >> game: anObject
	game := anObject

"protocol: #'mouse handlers'"

CardEventListener >> clickEvent: anEvent
	self game ifNil: [ ^ self ].
	self game chooseCard: anEvent currentTarget card
  

The CardEventListener>>#clickEvent: calls the Game>>#chooseCard: method when a user clicks on a card:

The CardEventListener has to be added to each CardElement object when a game view is initialized. The initialization happens in the GameElement>>#onUpdated method.

Here, we need to add one line to subscribe the listener:

"protocol: #hooks"

GameElement >> onUpdated
	self game ifNil: [ ^ self ].
	self removeChildren.
	self layout columnCount: self game gridSize.
	self game availableCards do: [ :aCard | 
		| aCardElement |
		aCardElement := self newCardElement card: aCard.	
		aCardElement addEventHandler: (CardEventListener new game: self game).
		self addChild: aCardElement ]
  

We just finished the last piece of code to be able to play a game with numbers:

| aGame |
aGame := Game numbers.
GameElement new game: aGame
  

Visual Animations

Bloc offers element animations using BlBaseAnimation BlTask subclass: #BlBaseAnimation uses: TBlEventTarget + TBlDebug instanceVariableNames: 'loop delay time startTime progress elapsedTime loopCount isStarted isRunning parent actionHandlers target eventDispatcher loopDoneTime' classVariableNames: '' package: 'Bloc-Animation-Basic' subclasses.

You can for example translate an element:

elementWithLinearTranslationAnimation
	<gtExample>
	| container element animation |
	
	animation := self linearTranslationAnimation.
	
	element := self animationElement.
	element relocate: 5@5.
	container := self containerElement.
	container addChild: element.
	
	element addAnimation: animation. 
	^ container
	 
    

You can also translate with more complex logic:

elementWithLinearTranslationAnimationWithDurationAndEasing
	<gtExample>
	| container animation |
	
	animation := self linearTranslationAnimationWithDurationAndEasing.
	container := self containerWithOneElement.
	container children first addAnimation: animation. 
	^ container
	 
    

In this tutorial we will use BlTransformAnimation BlAnimation subclass: #BlTransformAnimation uses: TBlTransformable instanceVariableNames: 'transformation fromMatrix toMatrix isAbsolute' classVariableNames: '' package: 'Bloc-Animation-Animation' to scale a CardElement when a mouse is over the card.

Enlarging Card

In the following part, we will learn how to enlarge a card when we move a mouse over it. To do so, we add a method to the CardEventListener:

"protocol: #'mouse handlers'"

CardEventListener >> mouseEnterEvent: anEvent
	| anAnimation |
	anAnimation := (BlTransformAnimation scale: 1.1 @ 1.1)
		absolute;
		duration: 0.5 seconds.
	anEvent currentTarget addAnimation: anAnimation
  

You can now test it playing the game:

aGame := Game numbers.
GameElement new game: aGame
  

Shrinking Card

As you noticed, enlarging works well, but we also need to shrink a card as the mouse leaves the card. To do so, we will use the same animation:

"protocol: #'mouse handlers'"

CardEventListener >> mouseLeaveEvent: anEvent
	| anAnimation |
	anAnimation := (BlTransformAnimation scale: 1.0 @ 1.0)
		absolute;
		duration: 0.5 seconds.
	anEvent currentTarget addAnimation: anAnimation
  

You can now test it again here:

aGame := Game numbers.
GameElement new game: aGame
  

Improving the Animation

The just written code has a potential issue. If you move your mouse fast enough, enlarging and shrinking animations will be in a queue at a same time, using BlElement>>#addAnimation: addAnimation: aBlBaseAnimation aBlBaseAnimation target: self. self enqueueTask: aBlBaseAnimation and executed in parallel. It means, that the CardEventListener>>#mouseLeaveEvent: animation is performed while the CardEventListener>>#mouseEnterEvent: animation is not over yet.

The same issue arises if the CardEventListener>>#mouseLeaveEvent: animation is not over yet and we add the CardEventListener>>#mouseEnterEvent: animation.

We can fix it by remembering what animation is in progress and stop it by calling BlBaseAnimation>>#stop stop self assert: [ self isRunning ] description: [ 'Animation is not running' ]. isRunning := false. "We have done an animation, notify listeners" self notifyOnFinishedHandlers before adding a new animation:

BlEventListener subclass: #CardEventListener
	instanceVariableNames: 'game animation'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Tutorial-MemoryGame-UI'.

CardEventListener class
	instanceVariableNames: ''

"protocol: #private"

CardEventListener >> stopAnimation
	animation ifNotNil: [ :anAnimation | anAnimation isRunning ifTrue: #stop ].

"protocol: #'mouse handlers'"

CardEventListener >> mouseEnterEvent: anEvent
	self stopAnimation.
	animation := (BlTransformAnimation scale: 1.1 @ 1.1)
		absolute;
		duration: 0.5 seconds.
	anEvent currentTarget addAnimation: animation

"protocol: #'mouse handlers'"

CardEventListener >> mouseLeaveEvent: anEvent
	self stopAnimation.
	animation := (BlTransformAnimation scale: 1.0 @ 1.0)
		absolute;
		duration: 0.5 seconds.
	anEvent currentTarget addAnimation: animation
  

Let's try it again:

aGame := Game numbers.
GameElement new game: aGame
  

Ready to Play

You just finished the tutorial. You can play the game executing the code snippet above.

Links to Pages containing missing references - allowed failures to allow references to missing classes and methods in the page.