Canastra (PHP version)

Canastra is a browser-based two-player card game that I’ve build in 2018, but nowadays I use the project as a playground for learning new skills. The game is not open to the public, but it sees weekly usage by my family.

This project was replaced by a newer version. Read the new write up.

Context

Canastra (or Canasta) is a card game that my significant other and I like to play on our spare time. Back in 2018 we both were living in separate countries, and I couldn’t find any online website/app that would allow us to play with our regional version of the rules.

Career-wise, I was in a stage that I understood but had limited experience with Test Driven Development, PHP 7.2 had been recently released and I was yearning for a more modern codebase, but my day job wasn’t yet there for it.

This gave me the perfect opportunity and the right motivation, so I decided to build it myself.

Architecture

The project is split into two components, canastra-lib, the game mechanics library, and canastra-frontend, the web project that handled all the user interactions.

canastra-lib

This is an object oriented PHP library that is responsible for managing the game state and registering the actions against that state, making all the validations needed (i.e.: order of the cards is valid, player couldn’t perform actions outsite their turn, etc). It was heavily tested: if my memory doesn’t fail me, I achieved 100% coverage on non-boilerplate code, but I unfortunately don’t run such reports anymore.

Some code excerpts:

1
2
3
4
5
6
7
8
9
// game state
class Game
{
    public function registerAction(Player $player, ActionInterface $action): ?ActionResultInterface;

    public function getStatus(): int;

    public function getCurrentPlayer(): Player;
}
1
2
3
4
5
6
7
8
9
10
11
// DrawFromDeck action
class DrawFromDeck implements ActionInterface
{
    public function perform(Game $game, Player $player): ?ActionResultInterface
    {
        [$drawnCard] = $game->getDeck()->draw(1);
        $game->getPlayerHand($player)->addCard($drawnCard);

        return new ActionResult\DrawFromDeck($drawnCard);
    }
}

canastra-frontend

I’ve started this component as a Symfony 4.0 project to handle all the user interaction (UI and public API) and convert user input into valid actions for the library. The UI is handmade (with bootstrap/jquery) built with webpack encore. I’m not a frontend developer by any means, so I did just enough to make sure it was playable.

game screenshot

The communication between the browser and the backend is done using WebSockets. Thanks to Ratchet I managed to make real-time interactions without relying on AJAX long pooling, and user actions are sent over the WebSocket as well. A background command spins up the WebSocket server.

websocket data transfer

An all-encompassing Docker image was built, initially using Jenkins pipeline, going though Bitbucket and GitHub Actions, and deployed to a VPS. Nowadays I’m managing the build and deploy processes via Ansible in a refreshed VPS.

Data storage had a straight forward start, serialize‘ing the game state into a file in disk. In 2019 I’ve moved the serialized data into PostgreSQL. In 2022 I’ve implemented the Event Sourcing pattern, storing events in PostgreSQL and rebuilding the state from the events as it’s read.

Event Sourcing data

Timeline

Important project events:

  • April 2018: development started;
  • May 2018: First beta released;
  • June 2018: 1.0.0 released;
  • November 2018: added PHPStan;
  • November 2019: PostgreSQL support added;
  • June 2021: upgraded to Symfony 5;
  • February 2022: upgraded to PHP 8;
  • September 2022: switched to Event Sourcing;

Future

This game has always been a playground for me to test different techniques and patterns at my own pace. At this point the game has reached maturity, so bugfixes and new features are worked on as the need arises. I’m still on the fence about opening the game to the public.

Because my day job involves TypeScript, I’m currently using the language to build a bot player for the game, connecting via the WebSocket, so I could potentially provide single player support in the future.