Last night, I was one of the speakers at the soon-to-be-known-as-renowned BrooklynJS. (If you live in NYC, it’s definitely a meetup to check out!)
For those who are a bit rusty, the basic rules of Spider Solitaire are as such:
- The ultimate goal is to remove all cards from the table.
- Cards can be stacked by order. So a 2 could be moved on top of any 3, regardless of suit, but not an 8.
- Any card can only be moved if the cards on top of it are in the proper numeric order, and ALSO are all of the same suit.
- The reserve cards can only be dealt if no columns are empty.
There are 50 reserve cards that are not initially dealt, and these can be dealt 10 at a time, with one card being dealt to each of the 10 columns on the board.
Now, these are a lot of rules to follow, so to get started I decided to break the game into small components, and focus on building it one small feature at a time. First came the deck and the cards. I decided each card had four attributes which were important. Numeric value and suit, which were important for gameplay, and face value and color, which came in later for aesthetics.
I then decided to break down the card behaviors into five different states.
- Active, Faceup Cards – this includes any faceup card that is not blocked.
- Facedown Cards – this includes any card that is facedown and not yet in play.
- Reserve Cards – this includes the 50 cards that will be dealt to the board later on, at the player’s discretion.
- Selected Cards – this includes any card that is chosen, and will be moved on the next click.
- Blocked Cards – this includes the cards which cannot be moved due to cards of different suits or non-consecutive values being placed on top of them.
These states are abstracted away a bit, so I then decided what classes events would be applied to, and what behaviors would happen on each click event. I didn’t worry about how to handle the reserve cards until after I got the rest of the game working, so we won’t go into those here.
Now that the initial planning was done, it was time to start building the game! I used the Gambit application built by Ashley Williams for the backend – this is a thin Sinatra application which builds a deck of cards based on a YAML file, and serves them to the client.
When dealing the board, I had my Sinatra view “deal” the cards directly into the DOM. Each column, card, and card attribute was its own div. Since I was using partials for the individual cards, I decided to start with all cards facedown.
This worked wonderfully for most of the game! But, later on, I ran into a problem. I was targeting only the cards on the board, and the reserve cards were not on the board until much later in the game. I found that when I dealt cards from the reserve piles, those cards would not be selectable since the click event was not applied to them. I did some investigating, and found out that to solve this, I could apply the click event to the columns – which would ensure that any card in the column would be able to be clicked regardless of where it initiated. I then passed the card class in as an argument, so that the action would be applied to the card, but the card wouldn’t be looked at until after the column was clicked.
So with what we’ve done so far, we have the ability to select cards, and that’s about it. I had an if statement, that would select a card if nothing was yet selected. The else of that statement was everything else in the game! That’s a lot of rules and logic! Again, I took it one step at a time and decided to break it down into little pieces.
The first piece of this else was what to do if a selected card was clicked again. That was easy enough, the .selected class had to be removed. But what if another card was clicked? Well, then the selected cards would be appended to that column, the uncovered card would have the .faceDown class removed, and the .selected class would be removed.
And that’s basically the game! …Except, the way we have it at this point, you could put any card on top of any other card. And any faceup card can be selected. It was time to build in validations.
In order to help with validations, I created a $firstSeleted variable. This is the selected card with the highest value. I then compared the numeric value of $firstSelected to the numeric value of the card clicked, and if the card clicked was one greater than the move was valid! Success!
Of course, we can’t forget about the suits and blocked cards. So we have to also add in a check, and if a card is placed on a card of a different suit, all cards above it are blocked. Here I ran into two roadblocks to work around. Initially, I only applied the blocked class to the cards that were a different suit from the ones on the top of the column. However, this still allowed any cards of the correct suit above the blocked cards to be selected, and this in turn made it really easy to cheat.
The other issue I ran into was that pulling the data from a YAML file added whitespace to either side of the suit. Initially I thought the amount of whitespace applied was uniform between all the cards, but I eventually realized there were some deviants leading to incorrect classes being applied, and so I had to strip out the whitespace for this comparison to be accurate.
Any card that is blocked also needs to be able to be un-blocked, or else the game will end pretty quickly, and so next I decided to build that functionality.
Whenever a selection was moved off of a column, I had the game look at all of the cards in that column that were blocked. Initially I found these cards by searching for any blocked cards within the column, but this returned the cards from the bottom of the deck to the top – the opposite order of what I wanted. I experimented with selectors and found that using $firstSelected.prevAll(“.blocked”) to target all of the blocked cards above the card that I had designated as $firstSelected returned the cards in the preferred order, and allowed me to work up the column.
The top card, which would be at index 0, will always become unblocked since it’s now the top card on the column. From there, I iterate over each card, moving up the column, and evaluating it to see if the suit of the card matches the one evaluated before it. If the suits match, the card is unblocked. As soon as it comes across a card that has a different suit, or a value that is not one greater than the previous card looked at, it stops checking and exits the function.
Now at this stage, we have all the basic functionality of the game board built in! We can select, unselect, move, block, and unblock cards. All that’s left is to figure out how to win! We win by removing all the cards from the board, and so we start by looking at the smallest case when any cards would be removed – when a complete set, cards A-K, is made all of the same suit.
To determine this, I built a function that is called each time a selection is appended to a new column. The column is checked to see if it contains a King. It then iterates through each following card, working down the column. It checks each card to see if it is the same suit, and if it has a value one less than the card above it. If it reaches an Ace, and the Ace is the same suit, then the game knows that it found a complete set, and it then removes that set from the board. Due to the way the game is constructed and when this function is called, we don’t have to check to see if the Ace is at the bottom of the column, because it always will be in this scenario. After the completed set is removed, the card that was under the King has to be unblocked or turned faceup.
Each time a complete set is removed from the board, another function is called to check and see if any cards are left on the board. If not, then you win! Congratulations!
The other parts of the game, which I’m not going into here, were figuring out the rules for the reserve cards and keeping track of the score. Future features I plan on adding include dragging cards, a timer which affects the score, and an undo button. The main thing I learned throughout this process was that breaking a large task into smaller, knowable components really simplifies the logic and makes it a less intimidating undertaking.
If you play the game and find any bugs or have other ideas for features, please open an issue on the game’s Github page!