Building the reusable parts
Keeping our design points in mind, let's go ahead and build the reusable parts. In the Organizer, create a new application called CardGameApp by selecting New from the Applications menu.
Building the Card part
Create the reusable Card part as follows:
1. Select New from the Parts menu.
2. Enter Card for the part class.
3. Select Nonvisual part for the part type.
4. Select AbtAppBldrPart for the Inherits from field.
5. Select OK.
The Composition Editor opens for the new part.
Adding the attributes
Switch to the Public Interface Editor view of the part. Add the following attributes by entering the attribute name, selecting the Defaults push button, changing the Attribute data type as indicated, and then selecting the Add push button.
To simplify this example we will not change the rank of a card after it has been initialized. Select rank from the list of attributes, remove its Change event symbol and select the Update push button.
The fullName attribute will be computed by concatenating the faceValue and suit attributes so the set selector is not needed. Select fullName from the list of attributes, remove its Set selector and select the Update push button.
Adding the actions
Switch to the Action tab. Add the flip action by entering flip as the Action name and selecting the Add with defaults push button.
Generating and modifying the default scripts
Select Generate Default Scripts from the File menu. Select the Generate all push button in the Generate Default Scripts window to create the scripts needed for this part.
Switch to the Script Editor view of the Card part and change the following scripts:
Note:
You can copy and paste the code in this book from the online version of this book into the Script Editor. The lines you need to change appear in boldface in the following scripts.
faceUp
"Return the value of faceUp."
faceUp == nil ifTrue: [self faceUp: true].
^faceUp
faceValue
"Return the value of faceValue when the card is face up.
When the card is face down return a blank."
faceValue == nil ifTrue: [self faceValue: ' '].
self faceUp ifTrue: [^faceValue].
^' '
faceValue: aString
"Save the value of faceValue."
faceValue := aString.
self signalEvent: #faceValue with: aString.
self signalEvent: #fullName with: (self fullName).
flip
"Perform the flip action."
self faceUp: (self faceUp not)
fullName
"Return the value of fullName which varies based
on whether the card is face-up or face-down."
^(self faceValue), ' ', (self suit)
icon
"Answers the icon representing the back of the card,
if the card is face-down. Otherwise answer the icon
representing the suit of the card."
self faceUp ifTrue: [ ^icon ].
^self flippedIcon
suit
"Return the value of suit when the card is face up.
When the card is face down return a blank."
suit == nil ifTrue: [self suit: ' '].
self faceUp ifTrue: [^suit].
^' '
suit: aString
"Save the value of suit."
suit := aString.
self signalEvent: #suit with: aString.
self signalEvent: #fullName with: (self fullName).
Save the Card part.
Building the CardDeck part
Create the reusable CardDeck part as follows:
1. In the Organizer, select New from the Parts menu.
2. Enter CardDeck for the part class.
3. Select Nonvisual part for the part type.
4. Select AbtAppBldrPart for the Inherits from field.
5. Select OK.
The Composition Editor opens for the new part.
Adding the attributes
Switch to the Public Interface Editor view of the part. Add the following attributes by entering the attribute name, selecting the Defaults push button, changing the Attribute data type as indicated, and then selecting the Add push button.
| |
deck | OrderedCollection |
faceUp | Boolean |
isEmpty | Boolean |
The isEmpty attribute will be computed by determining if the ordered collection of cards is empty, so the set selector is not needed. Select isEmpty from the list of attributes, remove its Set selector and select the Update push button.
The deck attribute will be read-only so the set selector is not needed. Select deck from the list of attributes, remove its Set selector and select the Update push button.
Adding the actions
Switch to the Action tab and add the following actions by entering them in the Action name field and selecting the Add with defaults push button.
• flip
• shuffle
Add the following actions by entering them in the Action name field, selecting the Defaults push button, completing their parameters as described below, and selecting the Add push button.
moveCard:to:
| | |
1 | aCard | Card |
2 | aCardDeck | CardDeck |
moveCards:to:
| | |
1 | anOrderedCollection | OrderedCollection |
2 | aCardDeck | CardDeck |
deal:to:
| | |
1 | count | Integer |
2 | aCardDeck | CardDeck |
Generating and modifying the default scripts
Select Generate Default Scripts from the File menu. Select the Generate all push button in the Generate Default Scripts window to create the scripts needed for this part.
Switch to the Script Editor view of the CardDeck part. Add random as an instance variable in the class definition. The random variable will be used to hold an instance of a pseudo-random number generator that will be used in shuffling the deck. Your class definition should now look like this:
AbtAppBldrPart subclass: #CardDeck
instanceVariableNames: 'faceUp isEmpty deck random'
classVariableNames: ''
poolDictionaries: ''
Now, add deck: and initialize private instance scripts as follows:
deck: anOrderedCollection
"Save the value of deck."
deck := anOrderedCollection.
self signalEvent: #deck with: anOrderedCollection.
self isEmpty ifTrue: [self signalEvent: #isEmpty with: true.].
The deck: script is private because we do not want other parts, except those that inherit from CardDeck, to use this method. This maintains the ability to change the implementation of the deck attribute to use an array in the future. This method updates the deck attribute and signals that the deck attribute has changed. If necessary, it also signals that the deck is empty.
initialize
"Create an empty deck and initialize the random number generator"
deck := OrderedCollection new: 52.
random := EsRandom new
Whenever a CardDeck part is instantiated at run time, VAST Platform sends the new CardDeck the initialize message. This provides a convenient point for any initialization required by the part. In this case, the deck is initialized to a new, but empty, ordered collection. The pseudo-random number generator used for shuffling a deck is also initialized.
Now, change the following public instance scripts:
deal: count to: aCardDeck
"Remove the first n cards from the deck and place them in aCardDeck."
| i |
count <= 0 ifTrue: [^self].
(count > deck size)
ifTrue: [i := deck size]
ifFalse: [i := count].
1 to: i do: [:index | aCardDeck deck addFirst: (deck removeFirst)].
"Force signalling of deck attribute changes for the card decks.
This is delayed until all cards have been removed for performance
and paint flicker reasons."
aCardDeck deck: (aCardDeck deck).
self deck: (self deck).
faceUp
"Return the value of faceUp."
faceUp == nil ifTrue: [self faceUp: true].
^faceUp
faceUp: aBoolean
"Change the faceUp attribute for every card in the deck."
deck do: [:card | card faceUp: aBoolean].
faceUp := aBoolean.
self signalEvent: #faceUp with: aBoolean.
self deck: (self deck)
flip
"Flip the deck over. What was the bottom card becomes the top card
and every card is flipped."
deck := deck reverse.
self faceUp: (self faceUp not).
isEmpty
"Return the value of isEmpty."
^deck isEmpty
moveCard: aCard to: aCardDeck
"Remove the input card and place it in the output deck."
aCardDeck deck addFirst: (deck remove: aCard ifAbsent: []).
"Force signaling of deck attribute changes for the card decks."
aCardDeck deck: (aCardDeck deck).
self deck: (self deck).
moveCards: anOrderedCollection to: aCardDeck
"Move the cards that are in the input ordered collection to
the output deck."
anOrderedCollection do: [:card |
aCardDeck deck addFirst: (deck remove: card ifAbsent: [])].
"Force signalling of deck attribute changes for the card decks.
This is delayed until all cards have been removed for performance
and paint flicker reasons."
aCardDeck deck: (aCardDeck deck).
self deck: (self deck).
shuffle
"Perform the shuffle action."
| card index |
deck == nil ifTrue: [^self].
1 to: (deck size) do: [:j |
index := (random next * deck size) truncated + 1.
card := deck removeAtIndex: index. deck addFirst: card.
index := (random next * deck size) truncated + 1.
card := deck removeAtIndex: index. deck addLast: card.
].
self deck: (self deck).
Save the CardDeck part.
Building the PlayingCardDeck part
Create the reusable PlayingCardDeck part as follows:
1. In the Organizer, select New from the Parts menu.
2. Enter PlayingCardDeck for the part class.
3. Select Nonvisual part for the part type.
4. Enter CardDeck for the Inherits from field.
5. Select OK.
The Composition Editor opens for the new part.
Creating the scripts
You will recall that the Card part has an attribute for an icon and a flippedIcon for each card. Creating an instance of CgIcon for each card, or even each deck, would waste system resources. We will implement the PlayingCardDeck part so that only a single instance of each icon is created when the application begins executing. These icons are used to initialize each card deck.
Switch to the Script Editor view of the PlayingCardDeck part. Add class variables FlippedIcon and SuitDictionary to the class definition. Your class definition should now look like this:
CardDeck subclass: #PlayingCardDeck
instanceVariableNames: ''
classVariableNames: 'FlippedIcon SuitDictionary '
poolDictionaries: ''
Whenever a VAST development image or runtime image is loaded and initialized, each VAST application (the software component) is sent the startUp message. This provides a convenient place to perform any application and class initialization. When the image closes each application is sent the exiting message so it can perform any necessary cleanup such as releasing system resources. We will take advantage of these messages to load the icons we need for the card deck.
Start by saving your image. This is important because you will be adding scripts that run when VAST image is starting. If there is an error in your scripts such that VAST Platform is unable to start, you will want to have a good image to return to. Do not save your image again until directed to do so.
Open the Script Editor on the CardGameApp part and add the following public class methods:
startUp
"The image is being loaded. Perform any necessary initialization."
PlayingCardDeck startUp
exiting
"The image is being unloaded. Release any OS resources being held."
PlayingCardDeck exiting
Close the Script Editor view on it.
Note:
Before you add the methods described below, make certain the abtico50.dll file is present:
o In a directory in your PATH for Windows
abtico50.dll contains various icons and bitmaps and is shipped with VAST. (For UNIX systems, abtico50 or abticons and the icons used by this application must be in a directory in your LIBPATH.)
Return to the Script Editor view on the PlayingCardDeck part. Add the following public class methods:
startUp
"Create a dictionary keyed by suit with associated icons."
| array i |
SuitDictionary := Dictionary new.
"Initializes the icons used for the cards."
array := Array new: 5.
i := 1.
#(9909 9908 9911 9910 9907) do: [:id |
array
at: i
put: (AbtIconDescriptor new
moduleName: 'abticons';
id: id;
icon).
i := i + 1].
SuitDictionary at: 'Spades' put: (array at: 1).
SuitDictionary at: 'Hearts' put: (array at: 2).
SuitDictionary at: 'Clubs' put: (array at: 3).
SuitDictionary at: 'Diamonds' put: (array at: 4).
FlippedIcon := array at: 5.
exiting
"AbtIconDescriptor class will free the icons, so reset the class
variables that use them to nil, so they are reinitialized."
SuitDictionary := nil.
FlippedIcon := nil.
Testing the startUp methods
Let's test the startUp and exiting scripts. Switch to the System Transcript window, enter the following expression, and run it by selecting Execute from the System Transcript window's pop-up menu: CardGameApp startUp.
Remember this statement CardGameApp startUp. If you exit VAST and return to the application in the future, you must execute the statement or you will get a walkback stating that SuitDictionary is not understood.
Next, inspect the PlayingCardDeck class by typing its name in the System Transcript window and selecting Inspect from the System Transcript window's pop-up menu. In the class inspector window, select classPool. You should see the following displayed in the right-hand pane:
Dictionary(Dictionary(a CgIcon a CgIcon a CgIcon a CgIcon ) a CgIcon )
These are the values of the SuitDictionary and FlippedIcon class variables. If you do not get these results go back and check your startUp scripts.
To test the exiting script, enter the following expression in the System Transcript window, and run it by selecting Execute from the pop-up menu: CardGameApp exiting.
Inspect the PlayingCardDeck class again. You should see the following displayed for classPool: Dictionary(nil nil ). If you do not get these results go back and check your exiting script.
Send the startUp message to the CardGameApp class once more to reinitialize its icons. Now you can breathe a sigh of relief and save your image.
Completing the scripts
Return to the Script Editor view on the PlayingCardDeck part and add the following private instance script:
initialize
"Private - initialize a standard deck of playing cards."
| face card gameValue |
super initialize.
face := #('2' '3' '4' '5' '6' '7' '8' '9' '10' 'J' 'Q' 'K' 'A').
(SuitDictionary keys) do: [:suit |
14 to: 2 by: -1 do: [:rank |
gameValue := rank.
rank > 10 ifTrue: [ gameValue := 10 ].
rank > 13 ifTrue: gameValue := 11 ].
card := Card new
suit: suit;
faceValue: (face at: rank - 1);
faceUp: true;
rank: gameValue;
icon: (SuitDictionary at: suit);
flippedIcon: FlippedIcon.
deck addLast: card]].
self deck: deck.
This script runs whenever a new instance of a PlayingCardDeck part is created. Notice that this method initializes its superclass before adding cards to the deck.