Implementing a Rock-Paper-Scissors game using Event Sourcing
Tags: eventsourcing, cqrs, eventdriven, ddd
Published: 2021-02-08 07:47:16

In this tutorial we will look at how we can design the game flow for a rock-paper-scissors game using Serialized APIs for Event Sourcing and CQRS.

Our favorite runtime environment for applications is usually [Dropwizard](https://www.dropwizard.io/) but since many out there prefer [Spring Boot](https://spring.io/projects/spring-boot) I decided to use it instead for this article.
Our [Java client](https://github.com/serialized-io/client-java) works with any runtime environment or platform you use
on the JVM.


## Configure the Serialized project

To develop our game we will use Serialized aggregates and projections. The aggregates will store the events for each game and the projections will provide a view of each game as well as a high score list of the top winners (in the case of multiple games being run).

If you have not yet signed up to Serialized you will need to [sign up for a free developer account (https://app.serialized.io). Once you've signed up and created your first project you will have an empty view of Aggregates, like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/110e22fxv9m3slgw01k3.jpg)

We now need to find out API keys which are available under **Settings**.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cxg0g6ad8qj2sazi2nsa.jpg)

Copy the access key and secret access key to a safe location. We will need these to access Serialized APIs from our backend application.

Great job! We now have an empty Serialized project. We're now ready to start developing our game!

## Setup and outline of the game

In this section we will describe the basic functionality of the game and the pieces that we need to implement to develop the whole game flow.

## Game rules

The core of the game are the game rules. If you are interested in the history of the game you can [read more about it on Wikipedia](https://en.wikipedia.org/wiki/Rock_paper_scissors).

This is a brief summary of the game rules:

- The game is played by two players.
- A round consists of both players showing their hand (rock/paper/scissors).
- Each round ends when both players have shown their hands and there is a winner.
- If a round is tied (same hand is shown), the round will be run again.
- Paper beats rock.
- Rock beats scissors.
- Scissors beats paper.
- A game is best of 3 rounds - it is finished when one player has 2 round wins.

## Game model

We will design our game using *commands* and *events*. The commands are actions that the game supports and the events are the [Domain Events](https://serialized.io/ddd/domain-event) that are emitted as results from these commands.  

### Commands

Given our simple game rules there are only two actions we can perform:

- `StartGame` will be sent when we decide to start a game with two players.
- `ShowHand` will be sent whenever a player shows their hand.

**Note**: since the game ends automatically when the last hand is shown, and we have a winner, this is not modeled as a command, but rather as an event instead.

An interesting consequence of designing the game with commands and events is that we get a clear picture of how there is a discrepancy between the number of different commands and events. 

### Events

Below is a description of the events that our `Game` aggregate emits as a result for successfully processing commands:

- `GameStarted` will be saved as a consequence of the `start-game` command.
- `RoundStarted` will be saved together with the `GameStarted` for the first round and implicitly for upcoming rounds when both players have answered.
- `PlayerAnswered` will be saved when a hand is shown by a player.
- `RoundTied` will be saved when both players have shown hands, and they show the same sign.
- `RoundFinished` will be saved when both players have shown hands
- `GameFinished` will be saved when we have a winner (2 or more rounds won for a player).

### Queries

We will also implement a number of queries to show the **Projection** support in Serialized. Projections help us calculate queryable models that are the result of a number of events that have been saved in the aggregates. The queries that we will support for the game are the following:

- Game status - the current status of the game (including the rounds that have been played).
- High score (wins per player in a list).
- Total number of games.

## Implementing the game 

Let's dive in to the implementation of our game!

## App configuration

If you are not familiar with our Java client, you can [read more here](https://serialized.io/java/get-started/) about the basics.

First we must configure the Serialized client. In our `AppConfig` class we create an injectable bean for the aggregate client that we will use to store events into Serialized. We use a `GameState` class to manage the materialization of the state from any previously stored events and register the handler methods in this class for each event type that we designed in our modeling session, respectively.

```java
@Configuration
public class AppConfig {
  
  @Autowired
  public AppConfig(Environment env) {
    this.env = env;
  }

  ...
  
  @Bean
  public AggregateClient<GameState> gameClient() {
    return AggregateClient.aggregateClient(GAME_AGGREGATE_TYPE, GameState.class, getConfig())
      .registerHandler(GameStarted.class, GameState::handleGameStarted)
      .registerHandler(PlayerWonRound.class, GameState::handlePlayerWonRound)
      .registerHandler(GameFinished.class, GameState::handleGameFinished)
      .registerHandler(PlayerAnswered.class, GameState::handlePlayerAnswered)
      .registerHandler(RoundStarted.class, GameState::handleRoundStarted)
      .registerHandler(RoundFinished.class, GameState::handleRoundFinished)
      .registerHandler(RoundTied.class, GameState::handleRoundTied)
      .build();
  }

  private SerializedClientConfig getConfig() {
    return SerializedClientConfig.serializedConfig()
      .accessKey(env.getProperty("SERIALIZED_ACCESS_KEY"))
      .secretAccessKey(env.getProperty("SERIALIZED_SECRET_ACCESS_KEY"))
      .build();
  }
  
}
```


## The game logic

The `GameState` class contains the state for our `Game` aggregate. When a new command (`ShowHand`) is sent to the `Game` aggregate, we will first load an instance of `GameState` based on all previously stored **events** that was saved on the game.    

```java

/**
 * The transient state of a game, built up from events
 */
public class GameState {

  private final Set<Player> registeredPlayers = new LinkedHashSet<>();
  private final Set<PlayerHand> shownHands = new HashSet<>();
  private final Map<Player, Long> wins = new HashMap<>();

  private GameStatus gameStatus = GameStatus.NEW;

  public static GameState newGame() {
    return new GameState();
  }

  public GameState handleGameStarted(Event<GameStarted> event) {
    gameStatus = GameStatus.STARTED;
    registeredPlayers.addAll(event.data().players.stream().map(Player::fromString).collect(toSet()));
    return this;
  }

  public GameState handlePlayerWonRound(Event<PlayerWonRound> event) {
    Player winner = Player.fromString(event.data().winner);
    long numberOfWins = wins.getOrDefault(winner, 0L);
    wins.put(winner, numberOfWins + 1);
    return this;
  }

  public GameState handleRoundStarted(Event<RoundStarted> event) {
    return this;
  }

  public GameState handlePlayerAnswered(Event<PlayerAnswered> event) {
    Player player = Player.fromString(event.data().player);
    shownHands.add(new PlayerHand(player, event.data().answer));
    return this;
  }

  public GameState handleRoundTied(Event<RoundTied> event) {
    shownHands.clear();
    return this;
  }

  public GameState handleRoundFinished(Event<RoundFinished> event) {
    shownHands.clear();
    return this;
  }

  public GameState handleGameFinished(Event<GameFinished> event) {
    this.gameStatus = GameStatus.FINISHED;
    return this;
  }

  ...
  
}

```

We will use a single aggregate `Game` to implement the rules for the game and to emit the proper events for the correct situations. Games are a good showcase for Event Sourcing since they require strong consistency. The preserved history that we get out of the box is also useful to build fun side-features such as high score and statistics. By reacting to events we can also easily build [notifications and reminders](https://serialized.io/guides/how-to-implement-notifications-and-reminders-using-reactions) for players to act on.

The `Game` class is our aggregate root that contains the implementation of the game rules and logic. 

```java
public class Game {

  private final GameState gameState;

  ...
  
  public List<Event<?>> startGame(Player player1, Player player2) {
    if (player1.equals(player2)) {
      throw new IllegalArgumentException("Cannot play against yourself");
    }
    Set<Player> players = Stream.of(player1, player2).collect(toCollection(LinkedHashSet::new));
    return singletonList(gameStarted(players));
  }

  Player calculateWinner(PlayerHand hand1, PlayerHand hand2) {
    if (hand1.answer.equals(ROCK)) {
      return hand2.answer.equals(SCISSORS) ?
          hand1.player : hand2.player;
    } else if (hand1.answer.equals(PAPER)) {
      return hand2.answer.equals(ROCK) ?
          hand1.player : hand2.player;
    } else
      return hand2.answer.equals(PAPER) ?
          hand1.player : hand2.player;
  }

  Player calculateLoser(PlayerHand player1, PlayerHand player2) {
    return calculateWinner(player1, player2).equals(player1.player) ? player2.player : player1.player;
  }

}
```

## Generating the high score leaderboard

The high score in our application is a **Projection** that is built up from all the `GameFinished` events that contain the id of the winner/loser for each game. 

We can build this projection by simply telling Serialized to transform our events into a queryable high score projection.

In our `AppConfig` we add a projection client that is used to initialize our projection definition when our application starts. 

```java
@Configuration
public class AppConfig {

  ...

  @Bean
  public ProjectionClient projectionApiClient() {
    return ProjectionClient.projectionClient(getConfig()).build();
  }
  
  ...
  
} 
```

We create a `ProjectionInitializer` service that will initialize all our projections (in this case the high score)

If the definition code is changed between app starts it will re-create the projection by re-reading all `GameFinished` events again.

```java
@Service
public class ProjectionInitializer {

  private final ProjectionClient projectionClient;

  @Autowired
  public ProjectionInitializer(ProjectionClient projectionClient) {
    this.projectionClient = projectionClient;
  }
  
  public void createGameProjection() {
    projectionClient.createOrUpdate(
      singleProjection("games")
        .feed("game")
        .addHandler(GameStarted.class.getSimpleName(),
          merge().build(),
          set().with(targetSelector("status")).with(rawData("IN_PROGRESS")).build())
        .addHandler(RoundFinished.class.getSimpleName(),
          append()
            .with(targetSelector("rounds"))
            .build())
        .addHandler(GameFinished.class.getSimpleName(),
          merge().build(),
          set().with(targetSelector("status")).with(rawData("FINISHED")).build())
        .build());
  }
  
  public void createHighScoreProjection() {
    projectionClient.createOrUpdate(
      singleProjection("high-score")
        .feed("game")
        .withIdField("winner")
        .addHandler("GameFinished",
          inc().with(targetSelector("wins")).build(),
          set().with(targetSelector("playerName")).with(eventSelector("winner")).build(),
          setref().with(targetSelector("wins")).build())
        .build());
  }

  ...
  
}
  
```
For the games projection we append rounds to a rounds array and modify the status of the game when `GameStarted` and `GameFinished` event are received.

The high-score projection is slightly different. In this case we use the `idField` feature of Serialized to create projections that are identifiable using the playerId instead of the `aggregateId` (which is the gameId in this case). This makes the high-score projections queryable by playerId.

## The application class 

Our main application class is the entry point for our Spring Boot application. It will start the web container and also initialize/update our projection definitions during boot. Here's how it looks:

```java 
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class GameApplication implements CommandLineRunner {

  private final ProjectionInitializer configurer;

  @Autowired
  public GameApplication(ProjectionInitializer configurer) {
    this.configurer = configurer;
  }

  @Override
  public void run(String... strings) {
    configurer.createHighScoreProjection();
    configurer.createGameProjection();
    configurer.totalStatsProjection();
  }

  public static void main(String[] args) {
    SpringApplication.run(GameApplication.class, args);
  }
}
```

Before starting the application you need to provide your Serialized API keys as system properties/environment variables to the Java process so that our `AppConfig` class can pick them up and initialize our client properly.

After starting the application, you can go to **Projections** in the Serialized console and you should be able to see the initialized projection definitions there.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nc3xl2ewtld2ij0sgsel.jpg)

## Putting the pieces together

To expose the game logic to our client (Web/Mobile/other) we will create a `@Controller` that receives HTTP `POST` requests (Commands)  from the client and executes our domain logic of the `gameId` provided in the request:

```java

@Controller
public class GameCommandController {

  private final Logger logger = LoggerFactory.getLogger(getClass());
  private final AggregateClient<GameState> gameClient;

  @Autowired
  public GameCommandController(AggregateClient<GameState> gameClient) {
    this.gameClient = gameClient;
  }

  @RequestMapping(value = "/start-game", method = POST, consumes = "application/json")
  @ResponseStatus(value = HttpStatus.OK)
  public void startGame(@RequestBody StartGameCommand command) {
    Player player1 = Player.fromString(command.player1);
    Player player2 = Player.fromString(command.player2);
    GameState state = GameState.newGame();

    Game game = Game.fromState(state);
    gameClient.save(saveRequest().withAggregateId(command.gameId).withEvents(game.startGame(player1, player2)).build());
    logger.info("Game [{}] started with players [{}, {}]", command.gameId, command.player1, command.player2);
  }

  @RequestMapping(value = "/show-hand", method = POST, consumes = "application/json")
  @ResponseStatus(value = HttpStatus.OK)
  public void showHand(@RequestBody ShowHandCommand command) {
    // Load the aggregate state from all events, execute domain logic and store the result
    gameClient.update(command.gameId, gameState -> {
      Game game = Game.fromState(gameState);
      return game.showHand(Player.fromString(command.player), command.answer);
    });
    logger.info("Player [{}] answered [{}] in game [{}]", command.player, command.answer, command.gameId);
  }

}
```

To expose the projections we create a `@Controller` that receives HTTP calls from the client and queries our projections and returns the results:

```java
@Controller
public class GameQueryController {

  private final ProjectionClient projectionClient;

  @Autowired
  public GameQueryController(ProjectionClient projectionClient) {
    this.projectionClient = projectionClient;
  }

  @RequestMapping(value = "/high-score", method = GET, produces = "application/json")
  @ResponseBody
  public HighScoreProjection highScore() {
    return HighScoreProjection.fromProjections(projectionClient.query(list("high-score").sort("-wins").build(HighScore.class)));
  }

  @RequestMapping(value = "/stats", method = GET, produces = "application/json")
  @ResponseBody
  public TotalGameStats gameStats() {
    ProjectionResponse<TotalGameStats> projection = projectionClient.query(aggregated("total-game-stats").build(TotalGameStats.class));
    return projection.data();
  }

  @RequestMapping(value = "/games/{gameId}", method = GET, produces = "application/json")
  @ResponseBody
  public GameProjection game(@PathVariable UUID gameId) {
    ProjectionResponse<GameProjection> game = projectionClient.query(single("games")
      .id(gameId)
      .build(GameProjection.class));
    return game.data();
  }

}
```

## Testing the application

To test the application you can send HTTP POST requests to the endpoints `/start-game` and `/show-hand`.

An example request to start a new game can look like this:
```bash
curl -i http://localhost:8080/start-game \
  --header "Content-Type: application/json" \
  --data '
{
	"gameId" : "dfd2d7bb-8c67-4d4f-87c1-364d72dfd05d",
	"player1": "Lisa",
	"player2": "Bob"
}
'
```

To play a round we will send two `show-hand` requests. One for Lisa and one for Bob:
```bash
curl -i http://localhost:8080/show-hand \
  --header "Content-Type: application/json" \
  --data '
{
	"gameId" : "dfd2d7bb-8c67-4d4f-87c1-364d72dfd05d",
	"player": "Lisa",
	"answer" : "ROCK"
}
'
```

```bash
curl -i http://localhost:8080/show-hand \
  --header "Content-Type: application/json" \
  --data '
{
	"gameId" : "dfd2d7bb-8c67-4d4f-87c1-364d72dfd05d",
	"player": "Bob",
	"answer" : "PAPER"
}
'
```

### Round results

Since our projection was already set up to show the results for each round we can now navigate to **`Projections/games/dfd2d7bb-8c67-4d4f-87c1-364d72dfd05d`** in the Serialized console and see the finished round.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gheo7lp6fyrho4p6ticz.jpg)

To access this data we also have a query endpoint in the application:
```bash
curl -i http://localhost:8080/games/dfd2d7bb-8c67-4d4f-87c1-364d72dfd05d
```

Which calls the Serialized `games` projection for the given game id and returns the (already) calculated projection data:
```json
{"players":["Bob","Lisa"],"status":"IN_PROGRESS","rounds":[{"winner":"Bob","loser":"Lisa"}]}
```

### Showing the high-score

If we play one more identical round where Bob wins the round (and hence also the game), we can see the high-score projection being updated with his win of the game:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/51t4gh7zrdmkyi0wnhjm.jpg)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/erfttckve5vqer7p3em4.jpg)

To access this data we also have a query endpoint in the application:
```bash
curl -i http://localhost:8080/high-score
```

```json
{"highScores":[{"playerName":"Bob","wins":1}]}
```

This query shows the projection data from the `high-score` projection and uses the `sort` and `limit` feature of the Projection API to show the 10 players with the most number of wins. After running a few more games with more players the response can look like this:

```json
{
  "highScores":
  [
    {"playerName":"Bob","wins":11},
    {"playerName":"Lisa","wins":6},
    {"playerName":"John","wins":5},
    {"playerName":"Dan","wins":4},
    {"playerName":"Anna","wins":1}
  ]
}
```

## Summary 

That wraps up the overview of this tutorial showing the basic techniques for Event Sourcing and CQRS using Serialized. Hope you find this tutorial useful and that it can inspire you to build your own application (or perhaps a game) using Serialized. 

## The complete sample code 

Check out the complete sample code for this tutorial in [our Github repository](https://github.com/serialized-io/samples-java/tree/master/spring-boot-rock-paper-scissors). Feel free to clone the project and modify it however you want. Have fun!