Implementing the Memory Game model
Let us start with the card model, Card
, an object holding a symbol to be displayed, a state representing whether or not the card is flipped, and an Announcer
to emit state changes.
For each set of changes displayed, please click on the checkmark button to accept the changes.
Object subclass: #Card instanceVariableNames: 'symbol flipped announcer' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-Model'. Card class instanceVariableNames: ''
After creating the Card
class we add an initialize
method to set the card as not flipped, and we provide several accessors:
"protocol: #initialization" Card >> initialize super initialize. flipped := false. announcer := Announcer new. "protocol: #accessing" Card >> announcer ^ announcer "protocol: #accessing" Card >> symbol ^ symbol "protocol: #accessing" Card >> symbol: anObject symbol := anObject "protocol: #testing" Card >> isFlipped ^ flipped
You should now be able to create a new card and initialize it with a symbol.
Click on the โInspectโ button to see the result in an adjacent pane.
aCard := Card new. aCard symbol: 7
Card Actions
Next we need two API methods, to flip a card and to make it disappear when it is no longer needed in the game:
"protocol: #actions" Card >> flip flipped := flipped not. self notifyFlipped "protocol: #actions" Card >> disappear self notifyDisappear
Looking at the Card>>#flip
method, the flip action will not work yet as the notifyFlipped
method is missing.
To test the Card>>#flip
method, you can try the following snippet, which will raise an exception:
aCard := Card new. aCard symbol: $A. aCard flip
Card Notifications
Card actions should notify subscribers about card state changes. They announce events of type CardFlipped
and CardDisappeared
, as follows in the notifyFlipped
and notifyDisappear
methods. Graphical elements will have to subscribe to these announcements as we will see later:
Announcement subclass: #CardFlipped instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-Model'. CardFlipped class instanceVariableNames: '' Announcement subclass: #CardDisappeared instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-Model'. CardDisappeared class instanceVariableNames: '' "protocol: #notifying" Card >> notifyFlipped self announcer announce: CardFlipped new "protocol: #notifying" Card >> notifyDisappear self announcer announce: CardDisappeared new
Now we are able to initialize a card and flip it:
aCard := Card new. aCard symbol: $A. aCard flip
Implementing Game Model
The game model, called Game
, keeps track of all the available cards and all the cards currently selected by a player:
Object subclass: #Game instanceVariableNames: 'availableCards chosenCards' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-Model'. Game class instanceVariableNames: ''
After creating the Game
class we add an initialize
method to create empty collections of available and chosen cards, and we provide accessors for them:
"protocol: #initialization" Game >> initialize super initialize. availableCards := OrderedCollection new. chosenCards := OrderedCollection new "protocol: #accessing" Game >> availableCards ^ availableCards "protocol: #accessing" Game >> chosenCards ^ chosenCards "protocol: #configuration" Game >> gridSize ^ 4 "protocol: #configuration" Game >> matchesCount ^ 2 "protocol: #configuration" Game >> cardsCount ^ self gridSize * self gridSize
At this moment we have a basic game object that has no cards:
Game new
Game Initialization
To initialize the game with cards, we add an initializeForSymbols:
method. This method creates a list of cards from a list of characters and shuffles it. We also assert in this method that the caller has provided enough characters:
"protocol: #initialization" Game >> initializeForSymbols: aCollectionOfCharacters self assert: [ aCollectionOfCharacters size = (self cardsCount / self matchesCount) ] description: [ 'Amount of characters must be equal to possible all combinations' ]. availableCards := (aCollectionOfCharacters asArray collect: [ :aSymbol | (1 to: self matchesCount) collect: [ :i | Card new symbol: aSymbol ] ] ) flattened shuffled asOrderedCollection
We can now try to initialize a game with capital letters:
aGame := Game new. aGame initializeForSymbols: 'ABCDEFGH'
Observing the Game
object above is definitely handy, but still cumbersome as we do not see the state of each card. We can improve it by overriding the Object>>#printOn:
method and adding a GtInspector
extension:
"protocol: #accessing" Card >> printOn: aStream self isFlipped ifNil: [ ^ super printOn: aStream ]. aStream nextPutAll: 'Card: '; nextPutAll: self symbol asString; nextPutAll: (self isFlipped ifTrue: [ ' (face side)' ] ifFalse: [ ' (back side)' ]) "protocol: #accessing" Game >> gtAvailableCardsFor: aView <gtView> self availableCards ifNil: [ ^ aView empty ]. ^ aView columnedList title: 'Cards'; display: [ self availableCards ]; column: 'Card' item: [ :aCard | aCard ] format: [ :aCard | aCard isFlipped ifFalse: [ aCard printString asRopedText foreground: Color gray ] ifTrue: [ aCard printString asRopedText ] ]
By executing the same snippet again, you will notice that the inspected result is easier to understand (select the Cards
view):
aGame := Game new. aGame initializeForSymbols: 'ABCDEFGH'
To simplify Game
initialization we define a dedicated instance creation method to create a game with a deck of numbered cards:
"protocol: #'instance creation'" Game class >> numbers ^ self new initializeForSymbols: '12345678' "protocol: #'instance creation'" Game class >> emoji ^ self new initializeForSymbols: '๐ฐ๐ก๐ ๐ช๐๐๐ธ๐' "protocol: #'instance creation'" Game class >> chinese ^ self new initializeForSymbols: 'ไธบไปๅ ฌๅฎถ้ๅฐไธชๆถ'
We can then use this instance-creation methods in a snippet like this:
Game numbers
Ready
Next, we will implement the game logic: Implementing the Memory Game model logic.
Links to Pages containing missing references - allowed failures to allow references to missing classes and methods in the page.