The Making of Solitaire for Mini Micro
Tags: miniscript, minimicro, programming, gamedev
Published: 2024-07-10 16:28:51

Over the weekend I got the urge to create a Solitaire card game for [Mini Micro](https://miniscript.org/MiniMicro/).  I completed it Sunday night, the day after I started it.  You can play it [here](https://joestrout.itch.io/solitaire), or download the source code [from GitHub](https://github.com/JoeStrout/ms-solitaire) 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](https://github.com/JoeStrout/ms-solitaire), you'll find three MiniScript (.ms) files:

- [solitaire.ms](https://github.com/JoeStrout/ms-solitaire/blob/main/solitaire.ms): this is the game itself.
- [title.ms](https://github.com/JoeStrout/ms-solitaire/blob/main/title.ms): this is the title screen/main menu.
- [startup.ms](https://github.com/JoeStrout/ms-solitaire/blob/main/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](https://miniscript.org/MiniMicro/index.html?cmd=run%20%22%2Fsys%2Fdemo%2FcardFlip.ms%22).  Clicking the card causes it to flip over, and pressing the left/right arrow keys rotates it.

![Screen shot of cardFlip demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/obf3qmsmjehb0ozmzxud.png)

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

![Animation of card flipping](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6tdvwz34iui6mmze4t0r.gif)

This requires some fairly complex math and some advanced features of Mini Micro's [Sprite class](https://miniscript.org/wiki/Sprite).  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 [import](https://miniscript.org/wiki/Import)ed 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](https://miniscript.org/wiki/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4b8nds3n9qc7rxhc0uxu.png)

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`](https://miniscript.org/wiki/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](https://github.com/JoeStrout/minimicro-sysdisk/tree/master/sys/pics/cards) which come from [Kenney's Boardgame Pack](https://kenney.nl/assets/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3i1eobextolm43qvbnxl.png)

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%.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qx12p2u7s7l60wvy1g5y.png)

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.

![120-pixel cards!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2apztidsmp7fuhzgljw7.png)

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](https://github.com/JoeStrout/ms-solitaire/tree/main/pics/cards-120)!

## 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.

![Animation of drawing three cards, with a slight delay between each](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hh8wm0lb0i3h9ns1hqpb.gif)

## 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](https://boardgamegeek.com/geeklist/42199/traditional-card-games-that-should-be-played-more), 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!