Over the weekend I got the urge to create a Solitaire card game for Mini Micro. I completed it Sunday night, the day after I started it. You can play it here, or download the source code from GitHub and play it in your own copy of Mini Micro.
Let's take a look at the code, and see what you could apply to your own card games.
Project Organization
If you go to the repo, you'll find three MiniScript (.ms) files:
- solitaire.ms: this is the game itself.
- title.ms: this is the title screen/main menu.
- startup.ms: this literally just runs title.ms (automatically when Mini Micro boots).
This is a pretty common organization for smallish games. Develop your game as a stand-alone program in one file (or split it into multiple files, if it's getting big or unwieldy). Then write another script for the title screen and main menu. This should run the game file (solitaire in this case) when the user clicks "Play", and conversely, modify your game script so that when the game is over, it runs the title script.
And finally, to ensure your game launches automatically when Mini Micro starts up, make a tiny startup.ms script that just runs your title script.
Building on CardSprite
There is a "cardFlip" demo included with Mini Micro. You can try it here. Clicking the card causes it to flip over, and pressing the left/right arrow keys rotates it.
When a card flips over, it does a neat 3D effect, with a proper perspective distortion during the flip.
This requires some fairly complex math and some advanced features of Mini Micro's Sprite class. Who wants to rewrite all that?
Fortunately, you don't have to! The cardFlip demo, like several others in the /sys/demo directory, is written in such a way that it can be used as an import module. You can tell this by pressing Control-C to break out of the demo, edit
ing the source, and scrolling to the bottom, where you will find:
if locals == globals then demo
This is a common way for a script to distinguish between when it is the main program — in which case, locals will indeed equal globals — and when it has been imported by some other script, in which case these will be different. So this says, if this is the main program, go ahead and run the demo. If not, then the script defines its classes and helper methods and so on, but doesn't actually do anything visible to the user.
In this case, what it defines is a CardSprite
class, subclassed from Sprite, which knows how to approach a "target" defined by:
- Position on the screen
- Sprite scale
- Sprite rotation
- Flip state (faceUp = true to face up, or false to face down).
It also has a speed
map that controls how quickly it approaches each of those target properties. So just by changing the target, and then calling update
on every frame, you can make a CardSprite move around the screen and flip up or down as desired.
But, this script lives in /sys/demo, which is not part of the standard importPaths. So how can we import it? The solution is just to check env
.importPaths
, and add /sys/demo if it's not already there.
if not env.importPaths.contains("/sys/demo") then
env.importPaths.push "/sys/demo"
end if
import "cardFlip"
(Note that contains
is not a standard Mini Micro method, but something added by the listUtil module, which we already imported at the top of the program.)
So now that we've successfully imported cardFlip as a module, we can access and subclass cardFlip.CardSprite
.
Card = new cardFlip.CardSprite
Much of the Solitaire is building out this class. We add convenience methods like goToInSecs
and goToAtSpeed
that let us more easily specify a target position and speed, to make cards fly around the screen in just the right way. We also add a moveToFront
method that ensures the card is at the top of the sprite list, so it appears in front of all other cards. Finally, there are some Solitaire-specific methods added here, such as canGoOnFoundation
and canGoOnTab
, which check whether putting a card in a certain pile would be a legal move in the game.
Using smaller/alternate cards
Mini Micro comes with a bunch of built-in playing card images which come from Kenney's Boardgame Pack. I love these card images, and when I started the project my goal was to demonstrate a game made using only these built-in assets.
Halfway through development, I realized that they're just a smidge too big. The cards are 140 characters across, and in Klondike Solitaire, you need seven columns of cards with at least a little gap in between. 7 times 140 is 980 pixels, but our Mini Micro screen is only 960 pixels wide. I tried to cram them in anyway, but it didn't look great.
Of course all Mini Micro sprites can be scaled at runtime, and that includes these cards. But the scaling algorithm it uses is "nearest-neighbor", rather than some form of blending. This doesn't produce a very pretty result with text and sharp icons, like you find on playing cards, especially when using an odd scaling factor like 0.8.
I really wanted this game to look nice and polished. So, I went back to Kenney's pack, loaded the vector (SVG) card image, and re-exported everything at a smaller scale (120 pixels wide instead of 140). Then I updated the code to load from my custom pics directory, rather than /sys/pics/cards.
CARDPICS = "pics/cards-120/"
...
Card.image = file.loadImage(CARDPICS + "cardClubsA.png")
Card.localBounds.width = Card.image.width
Card.localBounds.height = Card.image.height
This included the code in the createDeck
function that loaded all 52 cards:
Card.backImage = file.loadImage(CARDPICS + "cardBack_blue4.png")
outer.cards = cardDisp.sprites
// create the cards
ranks = ["A"] + range(2,10) + ["J", "Q", "K"]
for suit in "Clubs Diamonds Hearts Spades".split
for rank in ranks
card = new Card
card.speed = Card.speed + {}
card.frontImage = file.loadImage(CARDPICS + "card" + suit + rank + ".png")
card.rank = rank
card.rankVal = 1 + ranks.indexOf(rank)
card.suit = suit
card.red = (suit == "Diamonds" or suit == "Hearts")
card.black = not card.red
cards.push card
end for
end for
After those changes, everything else worked the same. You can see the difference if you click on the final image below, and compare it to the last one above.
If you're making your own Mini Micro card game, I encourage you to use the built-in cards if possible... but if you need slightly smaller ones, feel free to grab them from my repo!
Card Management
Next, some general notes about how to manage a deck of cards in a game.
In Solitaire, I kept all the playing cards, and only the playing cards, in one SpriteDisplay. I had another SpriteDisplay behind that for the wells (translucent dark areas indicating where you can pile up cards), and if I needed some UI elements (buttons etc.) on top of the cards, I would put those in a third SpriteDisplay above the cards. But having a display that's just cards lets me make the sprite list and my "all cards in the game" list one and the same. This was actually accomplished in the deck-loading code above, where it says:
outer.cards = cardDisp.sprites
So now my rule is: never remove a card from this cards
list, unless you immediately add it back. Removing a card from the list would remove it from the display, and also from the game. That would be appropriate in some kind of deck-building game where cards can be permanently removed from play, but for most card games, it's generally not something you want to do.
Then the gameplay consists of only moving these cards around on screen, arranging them in the desired order, and flipping them face-up or face-down. When you see something like a stack of cards for a draw pile, all the card sprites really are there — no need to get clever and try to remove the cards below the top one in the stack. Sprites are super efficient; Mini Micro can handle them all without breaking a sweat.
Keeping Things Moving
The only other technique that seems noteworthy to me, is how I ensure that all the cards can continue moving towards their targets. This requires calling update
on each card on every frame. So we start by making a function that does that for one frame:
updateCards = function
for sp in cardDisp.sprites
sp.update
end for
end function
But now, sometimes I want to wait a bit before doing something. For example, if playing the "hard" version of the game, cards are drawn 3 at a time. But I don't want to simultaneously draw 3 cards; I want to draw one, wait a bit, draw the second, wait a little more, and then draw the third.
If I just called the built-in wait
function for these delays, it would cause the cards I've already drawn to freeze during the wait. That's no good! So, I made this function:
waitButUpdate = function(delay = 1)
t1 = time + delay
while time < t1
updateCards
yield
end while
end function
Now we can call waitButUpdate
, which acts exactly like wait
, except that during the delay, it updates the cards on every frame. Now my draw-three-cards code can do stuff like
card.goToInSecs x, WASTEPOS.y, 0.5
card.target.faceUp = true
card.moveToFront
waitButUpdate 0.25
and wait a quarter second for this card to start its flip, before proceeding to the next card.
Conclusion
Making a card game in Mini Micro was a fun and easy project. Using the CardSprite class from /sys/demo/cardFlip, you can easily create very polished animations as your cards move around the table and flip over to face up or down. With the built-in card images, you can get started with most standard card games right away; but if you need to swap in a different set of cards, as I did here, that's easy enough to do.
Feel free to study and borrow from the Solitaire script for your own games, too. Many of the extensions made in its CardSprite subclass would be generally applicable to any card game. You can also see how I handled clicking and dragging cards with the mouse (see the clickDragCard
function), and adapt it to your needs.
Since Mini Micro was released, I've been hoping to see people creating card games with it. There are so many classic card games, the rules of which are almost always in the public domain, and players for which can be hard to find when you get an urge to play. That makes them ripe opportunities for computer adaptations!
So, here's hoping my little Solitaire game — and this behind-the-scenes write-up — will inspire you. Let me know what you think in the comments below!