This week on the stream I looked at writing a browser-based game that is similar to the fantastic Steam game Slay the Spire. Slay the Spire is a fun roguelike deck-builder game where you fight increasingly difficult monsters as you climb the titular spires. I decided to build a clone of this game in Laravel and React, mainly because the challenges of software design and architecture around game creation are fun and exciting. The main problems to be solved include:
- Deckbuilding and management
- Player state and turn-based combat
- Programmatic level design
- Enemy AI
All these things are hard to master, but it should be a lot of fun trying!
Stream One – Wednesday 28th September
Find the code at
https://github.com/GeeH/codename-trapdoor/tree/E01
I started by playing the game Slay the Spire and trying to produce a mind map of the game that I want to create in PHP and JavaScript. Using Miro I produced a map of the hugely simplified game that I’ll create. Writing this game is a daunting task, and so I decided that it would be more reasonable to produce a vastly simplified playable game initially, and then iterate on that game to add more features over time.
The simplified game will have cards that only have three stats, the energy cost to play the card, how much attack damage it does to the enemy, and how much defence it applies to the Player.
Players should also only have 3 effective stats, health, energy and defence (wrongly called Block in the stream).
Next, I thought about how I can produce this in the application with another mind map.
I’m asking questions early about what tech should be used to solve the different problems we face. I decided on Laravel for the backend because it gives so much out-of-the-box to solve common problems that many websites face. Who wants to code a login form and password reset code in 2019? I’ll use React for the front-end because I’ve done some work with it in the past and need an excuse to use it some more.
Interestingly I had a long conversation with the viewers who, from now on we’ll call Chat, around what to use for persistent storage. There are lots of options when rapidly prototyping an application, and sometimes storage isn’t needed at all. In this instance, I’m deciding where I can manage the many cards that make up the game. It’s entirely feasible to store the data for the cards in PHP arrays or YAML files for example, but ultimately Chat persuaded me that I’ll need a database at some point, and so I used MySQL along with Laravel’s useful migrations and seeder to define a couple of basic cards.
https://github.com/GeeH/codename-trapdoor/blob/E01/database/seeds/CardSeeder.php
Defining the database schema and initial seeded data like this is a great practice. You’re immediately putting your database into version control so that changes to schema and data can be code-reviewed just like any other changes. It also allows 3rd parties to quickly and easily get up and running with the project without too much hassle. Laravel’s migrations make this nicely straightforward, but if you don’t use Laravel, then Doctrine also provides a sound library for migrations.
A helpful tip I learned from Chat is that you can create the Eloquent model and the migration in one fell swoop using a single Artisan command:
php artisan make:model Card -m
The -m
switch tells Artisan to make the model and migration, which is very useful as it’s rare that you’ll want a model without migration and vice versa.
After working with models for a few minutes, I became increasingly annoyed with the lack of IDE completion in my favourite editor of choice, PhpStorm. It’s highly recommended to install the Symfony and Annotation plugins along with the Laravel plugin. The Laravel IDE Helper library is also a must-have to generate completion on your models and Laravel’s facades. With these installed and configured, I’m in a much better place now that PhpStorm is giving full completion and proper error reporting.
Finally, I get around to writing some code in anger! I took a stab at deciding how a Player should be modelled in PHP, and it raised some interesting questions.
https://github.com/GeeH/codename-trapdoor/blob/E01/app/Player.php
Initially, I used private properties and getters and setters to modify and retrieve values, but this looked and felt messy. The interesting part of the Player code came when I populated the Player’s starting deck in the constructor. This added logic and therefore should be tested. Funnily enough, when I wrote the test, it found a bug in the code immediately.
At the end of the stream, I’d written some more prototype code in the test class to see how interactions with the Player object would look during various phases of the game. I decided to refactor the object to have public properties and no getters and setters, but this looked and felt wrong too. Chat raised the very valid point that the public API on a Player class should reflect the actions the Player can take. Instead of having methods like setCurrentHealth
I should have actions like damage
. This makes the public API much more intuitive, and when used, you can see the flow of the actions much easier.
I decided to leave it there for that stream, and consider how the public API should look ready for next time. All in it was great fun, and we made much progress.
Stream Two – Friday 30th September
Find the code at
https://github.com/GeeH/codename-trapdoor/tree/E02
Before the stream, I decided to have a chat with Marco Pivetta about how I should structure the objects and where the logic should live. Marco is the maintainer of Doctrine and a very opinionated developer who I am lucky enough to call a friend. He convinced me that it’s fine to have a class that is mutable and has logic in it.
I’ve always thought that wherever possible, you should use value objects to store data in PHP. Value objects could be immutable and shouldn’t have logic in them, but I’ve been increasingly tired of classes that hold data and have a public API that only consists of getters and setters. The conversation with Marco was very useful in solidifying in my head where different code should live, and how the Game. Player and various Decks should interact.
For me, object design and interaction is one of the hardest parts of coding in object-oriented programming. Often I can’t see what classes are needed and how they should interact without getting some code down in my editor, which is why I spent the end of stream #1 prototyping some of the actions that should happen in the combat system.
To start the stream, I took the code that I prototyped last time and moved it into actual code that I’ll use in anger. I took the prototype code from the throwaway test I wrote last time and turned it into actual code and tests. This API is what I’ll use from now on, so it’s important to get it right.
https://github.com/GeeH/codename-trapdoor/blob/E02/app/Player.php
https://github.com/GeeH/codename-trapdoor/blob/E02/tests/Unit/PlayerTest.php
Some interesting things are happening here that needs discussing.
I’ve moved the Player’s stats into a new object that only has public properties called PlayerStats
. This cleans up the Player so that it’s more obvious what is going on with the Player state. The logic here is mainly around moving cards between the draw pile, the hand and the discard pile and we’re using Laravel Collections to store which cards are in which pile. The different piles are:
- The Deck – the different cards that the player has collected during this game
- The Draw Pile – the cards that the player can draw into their Hand, made up of the shuffled Deck at the start of combat
- The Hand – cards that the player can play during this turn of combat
- The Discard Pile – Cards that have been discarded either by being played or by ending the turn. All cards in the hand are discarded at the end of the turn
This API is looking much cleaner than before, and in the tests, we can see what is happening. Manipulating Collections is usually trivial, but I needed to do some unusual things. I accidentally fixed the tests on stream by mistyping “slice” for “splice” but realised that I could (ab)use the splice method with various arguments to remove things from the Collection and put things in specific places of the Collection. This is very useful in a deck-building game because the majority of the code we wrote on this stream was about manipulating cards and decks.
There are some problems with the existing implementation. The testCreateDrawPileFromDeckFillsTheDrawPileWithTheShuffledDeck
test fails when the shuffle re-orders the cards to the same order as the deck by pure luck. I documented this and want to make sure I fix this in the next stream.
I’m also calling some of the methods on Player to make sure the state is correct before calling the method under test. This is particularly noticeable in the testDiscardingHandMovesHandToTopOfDiscardPileWithNotEmptyDiscardPile
test. For me, this is an indication that there’s a problem in the code design somewhere. I need to be able to set the state of the Player before running the test without any Player code that isn’t under test running, to prevent false positives.
You can see from the commented out code that I started moving the default state being created in the constructor out into dependencies to let me set state in the tests. Chat tried to persuade me this was the right thing to do throughout the stream, and as usual, they were right, and I was wrong.
While it doesn’t look like I got a lot done in this two hours, it’s so important to get the API looking right early in projects like this that I don’t mind spending the extra time making sure it’s as good as it can be.
The fun of streaming for me is mob programming with the viewers, so if you’d like to influence how the game is designed and coded, then come and hang out during the live streams. If you’d like to know when I go live, you can follow me on Twitch, or on Twitter to get notified. If you can’t hang out while we’re coding, then leave a comment below, and I’ll consider your thoughts in future streams.