Play Pyramid Solitaire, a free online game that’s as engaging as it is challenging. Have fun and pass the time with this classic card game at Pyramid.com.
At first, I had built good rapport with Michael Cyger, head of this project, through various small projects. He approached me with the idea of working on the second version of this game and showed me an early Figma design of how the game would look like.
I accepted and we started work in late June and we deployed in August. Many challenges occured while implementing the project that took me by surprise but, in the end, we built a stable and enjoyable project.
At first, there was a problem with the first version of the game. The problem was simple to understand but difficult to solve. Basically, the first version generates games by copying a deck, shuffling it and dividing it into piles, the pyramid (28) and the drawer (24).
Since one cannot draw from the drawer after it has reached its end a third time, this meant that sometimes, users would lose on games that weren’t winnable in the first place.
Michael contacted me about this issue to see if I could implement a fix. I thought this would be easy to solve in a matter of an afternoon but upon implementation, I realized how hard it would be.
First, winnable decks do not emit any patterns. For example, one cannot determine if a deck is winnable or not through a mathematical algorithm. At-least, that’s not what I found.
Second, there is no limit to the amount winnable decks or better rephrased, the amount of possible decks is larger than the amounts of atoms on earth.
Of course, I didn’t have enough knowledge mathematics and physics to realize that and I spent much of the time debugging why is my Go program, the language of which is known for its speed & safety, is hanging forever.
Third, Go has tricky rules about referencing structures and manipulating slices(especially concurrently). Many times I glanced at my code, evaluated it in my head and came to the conclusion that nothing is wrong only to be surprised that one of my generated puzzles weren’t solvable.
A culmination of these facts and the fact that my algorithm was proven again and again to be wrong whilst I haven’t finished the game led to a lot of frustration.
After being tired of my code being more spaghetti like, I told Michael that I’d fix the issue and record each puzzle in the first month being solved according to my algorithm.
Given a pyramid, match the available pairs, draw a card if there aren’t any and continue doing so until the game is lost or won.
If the game is lost, create a new deck and run the algorithm until the game is winnable.
Algorithm No. 2 is an improvement on No.1 by cloning the Pyramid for each move and executing one of the moves as the first.
For example, if a pyramid has 3 possible moves, the original pyramid is cloned three times and on each clone, the move is incremented by one.
So pyramid 1 executes move 1, pyramid 2 executes move 2, pyramid 3 executes move 3.
Afterwards, for each pyramid, get the possible moves again and do this.
If a game has been won, send it to the concurrent channel(yes, the 2nd iteration of the implementation didn’t copy, didn’t use mutexes and was concurrent) and collect those in a slice.
Finally, return that slice. If the slice is empty, create a new deck and start over.
This means ignore the urge to do some cool concurrent design and instead use a boring, reliable design.
The only reason I did this was because algorithm no. 2, to nobody’s surprise, sometimes crashed(I had to use recover()
to get it to work without crashing), and generated faulty puzzles.
This improved the correctness of the algorithm but by a small amount.
Copy the deck, the piles, the card, move list, copy the whole file if you can.
This is so that I know what is causing issues and where.
This is practically the same as number 6 but with concurrency. The improvement in speed is substancial up to 20%.
Same as No. 7 but limit the amount of solutions to 10 so that you don’t wasting CPU cycles proving an already correct puzzle.
The take aways are as follows:
In the design I was going to implement, designed by Damien Terwagne, had elements I’ve never seen before, particularly a version of the game that runs on portrait and landscape on mobile.
Non CSS coders will read this somewhat puzzingly, out of everything possible feature in the Figma design, why does this stand out?
The answer is simple. One cannot simply specify “re-order this on mobile landscape” instead one has to define on non landscape, on mobile landscape and on non mobile landscape.
So, as an example, here’s code for a block that would be blue on mobile landscape and red on everything else(using tailwind):
<div class="bg-blue-500 landscape:bg-red-500 landscape:lg:bg-blue-500">
I should be blue if not on mobile landscape and red on mobile landscape.
</div>
Now where it gets tricky is on the previous design of the homepage, take a look:
Since the homepage completely ignores the supposed landscape/portrait, I had to use some PHP magic to remove it from the homepage. More than that, for the game, the elements cannot overflow which meant that for posts and pages like these:
would not be scrollable which breaks them.
Add to that the fact that I didn’t know how to utilize the landscape mode and get it to do what I want and the confusion ensues. Tailwind CSS doesn’t include these layout mechanisms (i.e. show something only on landscape mobile) as examples in the website. They don’t have a landscape page.
Streak calculation was, surprisingly, the hardest part about this project. On the one hand, I know for sure that streaks are necessary and if I didn’t implement it, I’d thwart a good amount of users from enjoying the game.
At the same time, I didn’t wanna run a php script loop that is inefficient since resources are valuable. I wanted to use the SQL implementation to calculate the streak.
My first idea was simple yet profoundly flawed: When a user plays a game, check the current streak in the database and update it to 1 if the previous streak was broken or if not, add 1 to the current streak.
The issue with this is we had to sync the unsigned users data when they login. For example, if a guest plays #3, #4 and #5, the game should sync the results of those game when they log in or register.
For reference, here’s what the table for calculating streaks looked like:
CREATE TABLE wp_potd_current_streak (
start int
end int
user_id int,
foreign key (user_id) references users(user_id)
)
I know, I know, not the prettiest sight. The problem was that this SQL code is not even correct. Many many times, it would simply display results that are incorrect.
After lots of thinking and researching, for some reason apparently nobody had thought of implementing streaks before or at-least wrote about them, I stumbled upon SQL cursors.
An SQL cursor is very simple: It is a cursor that is pointed at a particular row, you could advance the cursor and read its data or close it.
For example, say you have a cursor at row 5, you could advance it continously until you reach the end of all the available rows. Keep in mind that while you do this, you only have access to one row at a time so you could aggregate and do all of the cool things you wanna do.
What’s the catch, you might ask? SQL cursors don’t exist in WordPress’s wpdb. So it means we’re back to square one.
Of course, I had experience with using SQL rows in Go’s database/sql
but no matter how hard I searched, there was no equivalent in WordPress land.
Apparently, it is present within the standard PHP library PDO
.
SQL has always been a write-bottlenecked or even write-only language for me. Over the years, I purposely avoided writing SQL and would look for alternatives everywhere. I’d prefer mangodb, couchdb and others even if they were less performant just so I could have more flexbility with the schema and fetching data.
Regardless, I came across a cool little feature which was derived tables and SQL variables. Basically, MariaDB and MySQL don’t have a function called ROW_NUMBER()
(present in other databases like PostgresSQL) which calculates which row is calculated.
For the sake of demonstration, imagine we have a database full of the standard ANSI colors which are 10 in total. If we were to sort by color name ascending, the results would be like so:
+--------+
| color |
+--------+
| Black |
| Blue |
| Brown |
| Gray |
| Green |
| Orange |
| Purple |
| Red |
| White |
| Yellow |
+--------+
If we were to use ROW_NUMBER()
, the output would be like so:
+--------------+--------+
| ROW_NUMBER() | color |
+--------------+--------+
| 1 | Black |
| 2 | Blue |
| 3 | Brown |
| 4 | Gray |
| 5 | Green |
| 6 | Orange |
| 7 | Purple |
| 8 | Red |
| 9 | White |
| 10 | Yellow |
+--------------+--------+
If we were to sort by descending:
+--------------+--------+
| ROW_NUMBER() | color |
+--------------+--------+
| 1 | Yellow |
| 2 | White |
| 3 | Red |
| 4 | Purple |
| 5 | Orange |
| 6 | Green |
| 7 | Gray |
| 8 | Brown |
| 9 | Blue |
| 10 | Black |
+--------------+--------+
Now, the query for this doesn’t use ROW_NUMBER()
but the variables and derived tables, take a look at the following query:
SELECT ( @row_num := @row_num + 1 ) AS `ROW_NUMBER()`,
c.*
FROM colors c,
(SELECT @row_num := 0) n
ORDER BY c.color DESC;
It is this exist mechanism that allowed for the better streak calculation
Let’s define what a streak is: A streak is a the most amount of wins consecutively since today’s puzzles.
If somebody won today’s puzzles and yesterday’s but lost the day before yesterday, the streak would be 2.
To build the SQL query, we start with today’s puzzle number. We could retrieve it by fetching it from the algorithm. Afterwards, we would compare this value with the consecutive won puzzle numbers.
Now, one cool fact about SQL that I didn’t know about before embarking on this project is nested SQL SELECT
statements. Here’s an example:
SELECT color FROM (SELECT color FROM colors WHERE color NOT LIKE '_r%') ORDER BY RAND();
This SQL query fetches all the colors that don’t have the letter r
as its second character and after that it shuffles the colors.
So with this information, we want to fetch today’s puzzle and if it was won.
If it was won and the streak was not zero, add one to the streak, if not, return 1(i.e. the streak starts from today).
Then, calculate the streak. The streak would use MySQL’s version of ROW_NUMBER
but with a catch: It descends as the rows pass and it has a start value of today’s puzzle number.
If today’s puzzle number is 33 and we have five consecutive wins, the results would be like so:
+--------------+-----+
| ROW_NUMBER() | won |
+--------------+-----+
| 33 | 1 |
| 32 | 1 |
| 31 | 1 |
| 30 | 1 |
| 29 | 1 |
+--------------+-----+
However, the query we’ve described so far does not know if the wins are so consecutive, it only knows if the puzzles are, well, won.
The way to calculate if the wins are consecutive is to compare the ROW_NUMBER
to the puzzle_number
and the query should stop if they aren’t equal. When fetching the puzzle_number
, make sure that the order is by descending the puzzle number.
Finally, calculate the amount of rows you have and voilà, you have a correct streak.
Mid July to start of August, we had a lot of updates we wanted to apply to the mobile version of the game.
While desktops had the ability to invalidate the cache, mobile phones didn’t have any ability to invalidate the cache which meant our updates wouldn’t reflect to us and down the line, they wouldn’t reflect to the users.
First, I went through the google-closure-compiler
route which would collect the files, minify them both by erasing whitespace and renaming variables, remove sections of the code that is unused and finally, hash them using the in integrity
attribute.
This route proved unsatisfactory due to how the game is designed. You see, some scripts, particularly for the modals, are loaded asynchronously, which meant that they wouldn’t be included in google-closure-compiler
as the plan was to minify the base.
More than that, google-closure-compiler
’s ability to rename variables to shorten the overall file size meant that the base framework, Mithril JS, which is used for rendering the UI, wouldn’t work because it would be rename.
To help illustrate this, take a look at the following code:
m.render(document.body, m("p", "Hello from mithril"));
As a programmer, you’d probably assume that m
is defined and m.render
is also defined. google-closure-compiler
assumes both are not defined.
Furthermore, to fix it, you’d have to replace all instances of a global variable with window.globalVariable
, here’s a code snippet to illustrate this:
window.m.render(document.body, window.m("p", "Hello from mithril"));
However, after running google-closure-compiler
, the output would become something like this:
'use strict'; window.g.h(document.body,window.g("p","Hello from mithril"));
Which changes the window.m
to window.g
and window.m.render
to window.g.h
, further breaking the code. The solution to this is to refer to the field as a string like this:
window["m"]["render"](document.body, window["m"]("p", "Hello from mithril"))
Aside from the fact that this adds to the original size, admittedly by a small margin of 5% ish, it is rather messy.
Having very good knowledge of shell scripts, I stitched a script that listens to whenever any JS file is updated and executes the following script:
main.js
would become main.d4372324119fdb1b.js
.main.js
or main.hash.js
(where hash is any string) would be replaced with main.d4372324119fdb1b.js
.This solution took less time and was more obvious but I thought the compression benefit of google-closure-compiler
would be worth obtaining despite the broken behavior. At the end, we settled with this.
“Mithril JS is a modern client-side JavaScript framework for building Single Page Applications.”
Mithril JS was chosen for rendering the game’s UI for a couple of reasons:
This was my first project with Mithril JS, I thought it was a risky pick but it turned out to be stable and reliable. As soon I wrote the Modal component, Mithril rarely came in the way of me developing the UI. It was always due to my own mistakes that are easily observable. I didn’t spend more than 10 minutes on debugging.
More than that, what was really impressive to me was how readable the code was. Since we haven’t compiled the code down to a performant, minified and unreadable code using google-closure-compiler
(we might do it in the future), you could still visit the website and read the code line by line.
I can’t recall how many times I have researched a React-specific bug. But with Mithril, the laws are simple:
m.redraw()
Highly recommend this library. I should also add that whilst the documentation is rather sparse, it is concise and correct. With a small amount of tickering, you’d be a master in Mithril JS.
// A Mithril component is a function that returns an object
// with at-least a view function that returns a Mithril
// element
function Modal() {
return {
view(vnode) {
return m("div", {
style: "width: 100dwh; height: 100dvh; position: absolute; top: 0px; left: 0px; display: flex;"},
m("div", {
style: "width: 50%; max-width: 500px; height: 600px"},
vnode.children));
}
}
}
function HelloWorld() {
return {
view(vnode) {
return m(Modal, "Hello world");
}
}
}
The above component is an example of a component that another component as its base. This type of design is used quite a lot in Pyramid.com - with normal card being components and the drawer using the base card to stack two cards together. Modals, in Pyramid.com, are a good example of components.
I also converted different SVG icons to components so that when the file loads, the component is loads with it.
This was Pyramid.com - I hope you enjoyed the article and the game. I had a lot of fun and I learned a lot during this project.