The Making of Solitaire for Mini Micro

JoeStrout - Jul 10 - - Dev Community

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.

Screen shot of cardFlip demo

When a card flips over, it does a neat 3D effect, with a proper perspective distortion during the flip.

Animation of card flipping

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, editing the source, and scrolling to the bottom, where you will find:

if locals == globals then demo
Enter fullscreen mode Exit fullscreen mode

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:

  1. Position on the screen
  2. Sprite scale
  3. Sprite rotation
  4. 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.

Screen shot of Mini Micro code editor showing the start of the CardSprite class

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"
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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.

Full-size, 140-pixel cards

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.

Cards scaled to 80%.

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/"
Enter fullscreen mode Exit fullscreen mode

...

Card.image = file.loadImage(CARDPICS + "cardClubsA.png")
Card.localBounds.width = Card.image.width
Card.localBounds.height = Card.image.height
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

120-pixel cards!

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and wait a quarter second for this card to start its flip, before proceeding to the next card.

Animation of drawing three cards, with a slight delay between each

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .