Wsl Blog
Solitaire Fun
By Zach Wilder : Friday, February 16 2024
C / Cards

Way back in 2022 I started reading about using terminal escape codes to do away with depending on NCurses for all the terminal drawing and keyboard handling for terminal applications. If I recall correctly, my original motivation was because I wanted to use fancy unicode characters with the Goblin Caves project, and NCurses was not playing nicely.

Of course, I couldn’t just dive right into swapping over the entirety of Goblin Caves to using terminal codes directly without testing them on something - and the first thing I thought of was why not make a simulation of a deck of cards? I messed around, designed a way overbuilt deck of cards, and even started working on implementing a Cribbage game (ambitious!). I ran into a weird problem with Cribbage, couldn’t figure it out, and shelved the project. Some screenshots of the original project:

Well, when I recently started working on Goblin Caves again I remembered my efforts to write some simple drawing and event handling code, brought that code over to the Goblin Caves project, buffed it up, and it worked great. It needed some improvements, some modifications, and a little coercion to work as I expected it to… but it worked, and it not only got rid of the dependency on NCurses, it motivated me to fully separate the game from the drawing/event handling - so if I ever decided to play around with some graphics libraries (SDL) it would be painless to tie that in.

Sometime last month I was playing a game of Klondike (with a real deck of cards), and I started thinking about my playing card coding project. I opened it up, and decided to try and implement Klondike. Well, it went great, I made a ton of progress and then… I hit the same weird problem that I did with the Cribbage game. I looked into my (ridiculously overbuilt) data structure I used for the cards - couldn’t figure out what was going wrong, but I knew it had to be something there.

It hit me at work - while I was nowhere near a computer, of course. The problem the whole dang thing was running into was that it was using a linked list of cards to represent a deck of cards. Which, works great when you have one deck, and you don’t need to move cards between decks. What happens if you move all the cards from one deck to another deck? Well, the first deck doesn’t exist anymore. Now what if you want to move the cards back to the first deck?

The first deck doesn’t exist. That was the problem. Simple problem, but pretty catastrophic for that project, because the entire thing was built around the idea that the deck was “just” a linked list of cards. Knowing the answer to the problem, I started the project over again from “scratch”. This time, a deck is it’s own structure, and it contains a linked list of cards. The deck structure also has an id, and a count of cards in the deck. It seemed silly counting the cards each time I wanted to know how many cards were in the deck, especially since I could just update the count when I add/remove cards.

typedef enum {
    CD_NONE     = 1 << 0,
    CD_A        = 1 << 1,
    CD_2        = 1 << 2,
    CD_3        = 1 << 3,
    CD_4        = 1 << 4,
    CD_5        = 1 << 5,
    CD_6        = 1 << 6,
    CD_7        = 1 << 7,
    CD_8        = 1 << 8,
    CD_9        = 1 << 9,
    CD_10       = 1 << 10,
    CD_J        = 1 << 11,
    CD_Q        = 1 << 12,
    CD_K        = 1 << 13,
    CD_H        = 1 << 14,
    CD_S        = 1 << 15,
    CD_C        = 1 << 16,
    CD_D        = 1 << 17,
    CD_UP       = 1 << 18
} CardFlags;

struct Card {
    int flags;
    Card *next;
    Card *prev;
};

struct Deck {
    Card *cards;
    uint8_t count;
    uint8_t id;
};

That’s it! That’s the entire structure. Easy, simple. A “card” is just a collection of bitflags - which I’m a huge fan of, and really should write a blog post sometime about why they’re so useful. Literally every project I make ends up using them liberally.

The structure is complimented by the usual functions for creation/destruction, and adding/removing/moving cards. There is also a function for filling a deck of cards with the standard, 52 cards, and a function for shuffling the cards. Shuffling is accomplished by taking a random card from the deck, and moving that single card to the top of the deck - 500 times. This seems to sufficiently randomize the deck, and was quick and easy to implement.

Well, the cards and data structure are fun - but I still wanted to implement a full “game”. Klondike was the obvious choice, since that was what I was messing with in the original project.

The first challenge was figuring out a good, text based, interface for playing the game, and laying it out in a nice clean fashion. The original Cards project had each card as a 5x5 grid, with the suite on the top left/bottom right corner and the rank on the top/middle/bottom of the card. This worked well for showing the cards stacked vertically, but was kinda wonky stacking them horizontally. I played around with it, and settled on a 4x4 grid. The outside characters are the double line box drawing characters, and on the inside is the suite on the top left, and the rank on the bottom right. 10s are weird, and written in the middle. Since the cards are simplified, when they are stacked horizontally or vertically the rank and suite are just printed on the side/top of the card instead of the line drawing characters. To my eye, it looks and reads better than the 5x5 grid did.

To interact with each of the “decks” of cards (the stock, waste, seven tableaus, and four foundations), a “button” was added above the deck with a letter representing a key on the keyboard. The user presses that key, the button lights up, and the deck is active. The second deck the user selects signals where the user would like to move a card to. Simple enough!

Rule checking was easy to add - thanks to the bitflags. The cards need to alternate color, and be in sequence for it to be a valid move.

bool card_hearts(int card) {
    // Is the card hearts?
    return ((card & CD_H) == CD_H);
}

bool card_diamonds(int card) {
    // Is the card diamonds?
    return ((card & CD_D) == CD_D);
}

bool card_spades(int card) {
    // Is the card spades?
    return ((card & CD_S) == CD_S);
}

bool card_clubs(int card) {
    // Is the card clubs?
    return ((card & CD_C) == CD_C);
}

bool card_black(int card) {
    // Is the card black? (spades or clubs)
    return (card_spades(card) || card_clubs(card));
}

bool card_red(int card) {
    // Is the card red? (hearts or diamonds)
    return (card_hearts(card) || card_diamonds(card));
}

bool card_alt_color(int a, int b) {
    // Are the cards different colors? 
    return (card_black(a) != card_black(b));
}

bool card_same_suit(int a, int b) {
    // Are the cards the same suite?
    return (get_suite(a) == get_suite(b));
}

bool card_in_asc_sequence(int a, int b) {
    // Is the value of card 'a' exactly one higher than card 'b'?
    return (get_rank(a) == get_rank(b) + 1);
}

bool klondike_valid_move(int a, int b) {
    // Is the value of card 'a' one higher than card 'b', and
    // are they different colors?
    return (card_in_asc_sequence(a,b) && card_alt_color(a,b));
}

Everything worked well for moving single cards around following the rules, and the next step was to figure out moving sequences of cards around (super moves). This required a bit of thinking - or at least, I thought it did. The question I had was: how do I (the program) know which card the user is indicating from this pile they would like to move to another pile? The dirty answer is, I (the program) could just ask the user. That didn’t seem elegant enough, and would require a whole prompt/interpretation thing. I thought about this a lot… until I realized something pretty obvious - there is only ever one valid move from any tableau to another tableau, regardless of how many cards are in sequence. A red 7 will only ever accept a black 6 being placed on it - and in (standard) Klondike, if a card is visible it will always be in sequence. So, all I had to do was find the matching cards - which card in the “from” deck was a valid move to the “to” deck. This kept the interface simple, and didn’t require any extra effort from the user.

I decided to implement score keeping and a simple “high score” save system, using the same ideas as the original Windows 3.0 Solitaire - since Wikipedia had a nice table reminding me of what moves scored what points, this was a piece of cake to build in. The high scores are saved in a binary file in the same directory as the application - nothing fancy.

This project utilized many of the “Toolbox” files I started collecting. I also polished up some of the drawing functions in the Goblin Caves project (the ones for drawing simple rectangles, message boxes, and menus), made them portable, and brought them over pretty easily. The drawing functions aren’t used yet, but will be built into things like the options screen, a high scores screen, game select…

Some improvements that I feel could be made, since everything is actually “done” with Klondike:

The files are, as usual, all up on Github.


[Click here to comment on this post!]
«« Last Time: Mieke, Part 1

[Click Anywhere To Close]