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 Object subclass: #Announcer instanceVariableNames: 'registry' classVariableNames: '' package: 'Announcements-Core-Base' 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: printOn: aStream "Append to the argument, aStream, a sequence of characters that identifies the receiver." | title | title := self class name. aStream nextPutAll: (title first isVowel ifTrue: ['an '] ifFalse: ['a ']); nextPutAll: title method and adding a GtInspector GtInspectorObjectElement subclass: #GtInspector uses: TGtPagerWindowOpener instanceVariableNames: 'contentElement playgroundElement titleNotifier playgroundPageStrategy databasesRegistry externalSnippetContext' classVariableNames: 'IndexableDisplayLimit' package: 'GToolkit-Inspector-! Core' 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.