It's been a couple of days since I finished building Where's Scorpion game. It's a hidden object game and works similarly like a photo tagging app. I had built this small pet project for about 3 or 4 days. And, as usual, I posted it on some related subreddit communities after completed the project. I received some enthusiastic responses, which made me happy of course. I was also surprised when looking at the leaderboard that many people even played the game competitively (I'm pointing at you, Kirbymasters87). 😅
The project would not be possible without Gus Morais' beautiful artwork. I was super lucky when I stumbled across his posters looking for some Where's Waldo-style images.
Now, let's turn into some technical details related to this project. First, I really recommend you to read the first post of this blog, Stacking Up a Blog, because the core tech stack is almost the same and I would not repeat myself here.
TL; DR
I tell a high level explanation about:
- the app features
- the database schema
- managing game sessions
All of the app core features, like commencing the game, validating the tagging, and checking the game status, are on the backend. Therefore, it is impossible for the users to cheat the game, for example, by inspecting the client source files and fetch requests in the DevTools or by modifying the game records. All the frontend does is just to show the user interface and submitting forms, like how normally frontends do. Securing the app this way comes with some drawbacks, though. For example, the client has to communicate a lot with the backend, which is very slow especially using a free plan server like in my case 😅. But I tried my best to put some pending UIs to update the game status while not interupting user gameplay at the same time. See the complete code implementation on my GitHub.
The following image shows the database schema for this app.

The Board table is to store the board image, which is our beloved Gus Morais' Mortal Kombat poster saved in Cloudinary storage. I still had to make this table although it only has one record so I don't have to worry about database migration in case I add more boards in the future.
The Character table is for storing character names, images (with Cloudinary link too), and locations. The location is defined as a rectangle, which requires four vertices: x1, y1, x2, and y2. I prefer using JSONB as the location data type instead of creating a new location table because I always interact with the location data as a whole and the data is tend to be unique so I guess creating its new table is inefficient.
The History table is for the playthrough records. The record is created whenever a user start or restart the game, then it will be modified throughout the game.
The Task and Progress tables are tied to the history record and useful to capture the game status for the characters that have to be found and have been found. The position field in the Progress table is a user-registered click position on the board defined by coordinate x and y, and stored as JSONB.
What's the point of creating both task and progress tables if we could compute the progress by using the task and character data?
That's actually doable, but it can compromize the app performance caused by the additional computations, and I don't want that, especially in a game app. Therefore, it is actually just a preferential matter of time vs space constraints.
One important thing about the coordinates is that I use relative coordinates which means their values are relative to the board dimension, i.e.:
{
position: JSON.stringify({
x: (pos.x / pos.width) * 100,
y: (pos.y / pos.height) * 100,
})
}
This definition makes the coordinates keep relevant in different media sizes.
I understand that almost nobody wants to put their data on the internet if they don't see any valuable purposes. That's why I don't want users to log in first to play the game. However, I still want to keep their game sessions so that they can still continue the game in the same state where they left off the app. Therefore, I used JWT to give them tokens that they can store in their local storage. The token payload only contains a History record id which can connect to our persistent History table so they eventually can get their latest game status. Later if the user completed the game, they can put their name in so their record can be visible on the leaderboard.
As usual, the frontend is where I spent most of my time to code. The client side implementation is very technical and tied to the frontend framework of choice (I use React Router Framework Mode). So I don't really want to discuss it here but if you wonder about it, you can still explore it on my GitHub repository.
Well, that's some high level description about this app. Let me knows what you think in the comments.
