Sidi Barrani

A realtime online multiplayer card game

Backstory

Before the COVID pandemic, we often played a Swiss traditional card game called "Jass" with my family. Many people in Switzerland know how to play this game despite its considerable complexity. We also taught my Spanish girlfriend how to play it, and she loved it. During the pandemic, we couldn’t play as much anymore, and though there are plenty of apps and websites for standard modes of Jass, I decided to create my own adaptation. I chose a special play mode called "Sidi Barrani" as the base of the game. At the time of this writing, my implementation is the only one available online.

So how does this game work?

Sidi Barrani is played like the traditional Jass (Schieber, for the savvy ones amongst us). What is special about Sidi Barrani, is that it adds a gambling stage before the actual game, where players already see their cards and can place bets on the outcome of the game. To place a bet, players select a play mode and select the amount of points they think they will be able to make in this mode. Ideally players chose the mode that best suits the cards they have been dealt. The higher the bet, the more likely you are going to be allowed to play your game, but also the more risk you are taking of awarding the bounty to the enemy team.

If in any round of betting, all players have skipped, the highest amount bet will be allowed to play the game in their selected play mode.

After the play mode has been determined in the gambling stage, the game transitions into the play stage. Players take turns in playing one card each. After each round, the highest card wins and the points of the stack are awarded to the winning team. This goes on until all the cards have been played. Cards have different strengths depending on the play mode:

The round ends when all cards have been played. If a team manages to achieve their bet points, they will be awarded the points. If not, the enemy team is awarded the points. After the round has ended, players are shown a summary screen of each turn of the round, as well as the current result of the game. The game has finished if one of the teams has achieved the win condition, otherwise it will transition into the next round.

Technology

One goal of this project was to explore new technologies. Here’s the tech stack:

At the heart of this game is an AWS Amplify project that was bootstrapped using the Amplify CLI. The process is technically pretty straight-forward, you can even choose some templates based on the frontend framework you want to use for your app. Even though Amplify is generally designed to be very simple to setup (it should apparently enable frontend developers to handle full stack apps without the headaches), I managed to break the thing in a myriad of different ways that require to wipe everything clean and start from scratch several times. There are some subtle intricacies especially when running multiple different environments for staging and production. I also found it quite confusing that there are multiple different dashboards within AWS that achieve the same thing. There is the Amplify specific UI and then there is a UI for each individual component that the Amplify CLI set up (e.g. DynamoDB, Cognito user pools etc). Some actions can be taken via the Amplify UI and some others have to go through the dedicated resource UIs. It took quite some time to wrap my head around this. In the end I went with the dedicated UIs most of the time and pretty much stopped using the Amplify UI altogether.

Rough architecture diagram

Overall I found Amplify to be quite a painful experience, especially if an environment ended up in a broken state (most likely due to my wild and uninformed mutations to the infra setup). When you try to push a new configuration with the CLI only for it to break and roll everything back after 10 minutes of waiting, it is quite disheartening. I also have to admit that most of this pain came from me not understanding the AWS cloud at all in the beginning. Amplify tries to abstract this complexity away from the user, but it only works until you run into problems. Once things stop working after you run your CLI commands, you have to dig into the actual cloud infrastructure, and at this point the multiple UIs and ways of achieving the same thing didn't help my confusion. In the end I managed to get a setup running that works relatively reliably though and once I roughly understood how everything plays out together, it's nice to use.

The overall setup consists of a Cognito UserPool and an AppSync GraphQL API that's backed by python lambdas writing into DynamoDB.

Serverless Architecture for a Game?

The main game logic resides in a single python lambda, which means we don't have a traditional server running. This has some implications for a game: most games have some notion of state and actions that modify this state. Sometimes actions are triggered by user interactions, and sometimes they should be triggered based on timers. One example of this could be that we want to remove the cards from the stack once the round is completed, but we want to wait some seconds after the last card was played, so the players can see the final stack before removing it. AWS lambda functions are invoked upon requests from the client. This means that in the end, each request is either triggered by a user interaction or a client side timer. There is no easy way to implement a time based action trigger in a serverless lambda function. There are ways using message queues, but at the time when I implemented this, Amplify did not support message queues. Therefore i went for the simplest solution, which is to issue the requests from the client based on client side timers. Since Sidi Barrani is a multiplayer game, there will be client side timers for each of the players, which can potentially result in the same request being fired from all participating clients at roughly the same time, so we need to properly handle race conditions on the server side.

Game State and Realtime Updates

In a multiplayer game, we need to have some form of realtime updates. When one player plays a card, we need to update all the other participating player's clients with the new game state. For this, Amplify came in really handy. It supports GraphQL subscriptions out of the box. The way I set it up was as follows:

I defined a GraphQL subscription onGameStateChanged that can be invoked to notify connected clients about a change in the game's state. For each of the standard game actions such as play card, place bet, skip bet and so on I implemented a GraphQL mutation that is routed into custom request handlers within the python lambda containing the main game logic. Each of these request handlers returns the game instance after it has completed its mutations to the database. The GraphQL mutations then automatically invoke the subscription. Each client gets notified about the game state change, and then re-queries its specific state. This is an important detail; we do not want each player to receive the cards for every other player in the game in their game state queries. That would allow people who know how to inspect a network request to see other players' cards. Therefore, the subscription only notifies and the client then issues a separate query for the actual game state. In this query, the client submits the authenticated users' token, and we can return only the data they are supposed to see.

Making it shiny

I wanted this game to have stunning visuals. Card games are relatively simple in terms of their game mechanics. When it comes to visualizing what is happening, it can get pretty complicated though. The first thing i needed was the actual game card visuals. Luckily there are free SVGs available on Wikipedia. Sidi Barrani, like most Jass games uses 36 playing cards, from the 6es through to the Aces. I created simple components in Vue for each one of the cards.

The look and feel of the cards

The cards in the game then always have a position on the field. This position is a simple 2D coordinate with an x and y component. By default, the cards animate their position when it is set using a spring animation. This means if the a card is currently at (0,0) and we set its position to (50, 50), it wont immediately jump there but transition to the new position according to the spring's properties. Spring animations feel very natural and this gives the overall game a nice touch and feel. I also added a hover effect to the cards that are in the player's hand, which makes them appear as if they were an actual 3-dimensional object.

The rest of the UI is styled using TailwindCss, a library i haven't used before but which proved to be extremely powerful in creating a consistent and coherent visual appearance.

So what does it actually look like?

The home page allows the user to create a new game or join any lobby of publicly listed games that are still looking for players.

The landindg page where users can join existing lobbies of other players

Here, a user called Fipser has created a game in mode Duo and is looking for an adversary. When clicking the game, we can enter its lobby. In the lobby, we can then join either team. This game is set to the Duo mode, where each team consists of a single player. We can now join the other team, and this game is then ready to be started. We can also choose the color of our team, as well as change the game mode and set the win condition of the game. Here it is set to 1000 points. The team that first reaches this amount of points will win the game.

A lobby of a game with the game's settings

Once we are ready, we can hit the Start button which will bring us to the betting stage of the first round. This is the stage where the players already can see their own cards (partially in Duo mode) and can place bets on the outcome of the round.

Betting round where players gamble for a specific play mode based on the hands they have been dealt

The players can now place bets until a maximum count of 157 points is bet by one of the players or both of them have passed. Once either of those conditions is matched, the game enters play mode, where players try to match their bet or prevent the other team from matching theirs.

Playing in the previously determined mode until all cards have been played

Once all cards of the round have been played, the players are presented with the current game results as well as a summary of what has been played in this round and how many points were awarded for each stack

End of round summary and results display

In this round, player Fipser has staked 70 and bet to win with diamonds. They made 151 points in the game, which is more than the 70 they staked, so they are awarded also +70 points for the bet. This is where the game ends once one of the players has reached the minimum amount of points required to win the game.

The game is currently publicly available for everyone to try out here. As long as my AWS bill doesn't explode, i will keep it that way 😁