This game is entirely based on events, and it uses Apache Kafka as a message broker. It allows the players to be offline during the game and, as soon as they get back, it makes it possible for them to keep on playing as if they were never offline.
To start it, one must use the docker-compose.yml
file that is placed in the docker
folder or, if convenient, use the script startInfra.bash
under the scripts
folder.
These scripts are aimed at easing the burden of trying to figure out what are the possible options to start the system.
They are the following:
buildJar.bash
: build the application locally, usinggradlew
as wrapper (which, in turn, will download Gradle if needed)startInfra.bash
: will invoke Docker compose to bootstrap the infrastructure needed to run the system. As a Docker tool, will download Docker images if needed.startPlayer1.bash
/startPlayer2.bash
: will spawn instances of players namedplayer1
andplayer2
, respectively. These are definitions that will reflect both on the infrastructure side (because they will attach to Kafka in topics that are in accordance with this name) as in the domain side (because it will output to the terminal reflecting it).
The architecture used here is an onion, similar to the one seen on Jeffrey Palermo's blog. It is not using the same definitions, though: the package structure used here is:
interfaces.incoming
: Represents the data that comes to the system and, therefore, is the outer layer of the onion. As such, it may only access the layer that is immediately below it or itself, but not layers that are below the one that's immediately below.domain
: Represents the domain of the game. Like theinterfaces.incoming
package, it also cannot access layers other than the one that is immediately below, and also cannot access any layer that is above it.interfaces.outcoming
: Represents the data that is leaving the system. As it is the last layer, it cannot access any other layers than itself.
Usage of DDD is constrained to the domain
package. Classes there try to communicate to each other like real-world objects would try to communicate. So in general, players talk to each other by telling them what the game data is, and it is possible for each of them to recreate the other one's data by applying the given modifications.
There's a fundamental difference between a human playing and a computer playing. Often, the human will just try allowed modifiers to check if there's a fit. If not, he/she will just keep trying until figure out what is a valid modifier.
The computer player will not apply this behavior; it will rather try some math trick in order to figure out what is a feasible modifier.
- Avoid using dependency injection through fields, but rather via constructor. This way, objects can ensure their own initial state without the need to rely on any framework (in this case, Spring)
This project has been developed using SonarLint. Therefore, several changes to the code have been applied due to the plugin's recommendation. The most evident is turning test classes no longer public.
Also, this project uses Spring's formatter plugin. As such, before doing any checks it verifies if the code is following the plugin's standards. To apply these standards, just run ./gradlew format
.
- Test data should be random where possible (to avoid using test data that leads to skewed tests)
- Also, the tests use as much as possible the fixture test pattern
- The architecture itself is unit-tested through the use of the ArchUnit framework
Jacoco is configured to prevent builds if coverage levels are below 100%. This can be configured in the build.gradle
file.
Contract tests are defined by using REST-assured and Spring for Apache Kafka.