This is a documentation for Board Game Arena: play board games online !

Zombie integration testing

From Board Game Arena
Revision as of 21:39, 23 September 2022 by Mogri (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

So you'd like to test your PHP

To an end user, BGA's service calls are something of a black box. Wouldn't it be nice if you could run an automated test to check if your game is completely broken without having to go through each turn manually?

Good news: the framework already forces you to do most of the work required to make this possible. Introducing: integration testing... with zombies!

Getting started

In order for your automated test to run successfully, you need to have some states and actions implemented in your PHP code, and it should not surprise you to learn that you need zombieTurn working for those states and actions. You do not need any UI code for this to work. We will invoke our test via the chat pane. Here's our starting point:

  function callZombie($numCycles = 1) { // Runs zombieTurn() on all active players
    // Note: isMultiactiveState() doesn't work during this! It crashes without yielding an error.
    for ($cycle = 0; $cycle < $numCycles; $cycle++) {
      $state = $this->gamestate->state();
      $activePlayers = $this->gamestate->getActivePlayerList(); // this works in both active and multiactive states

      // You can remove the notification if you find it too noisy
      self::notifyAllPlayers('notifyZombie', '<u>ZombieTest cycle ${cycle} for ${statename}</u>', [
        'cycle'     => $cycle+1,
        'statename' => $state['name']
      ]);

      // Make each active player take a zombie turn
      foreach ($activePlayers as $key=>$playerId) {
        self::zombieTurn($state, (int)$playerId);
      }
    }
  }

This will run through N turns/rounds/phases of the game with all players taking a zombie turn. However, if you've written your server-side code correctly, this will fail as soon as it tries to take someone else's turn.

It's because of checkAction

Your action handlers probably look something like this:

  function actionPlay($someParams) {
    self::checkAction('actionPlay');
    $playerId = self::getActivePlayerId(); // or maybe getCurrentPlayerId, which is worse for our purposes
    // Do something
  }

checkAction will always verify if the current player is able to take the given action. When we invoke a command via the chat pane, the current player is the one typing the command.

So our next step is to update any action handlers a zombie will need, adding an extra parameter:

  function actionPlay($someParams, $zombiePlayerId = null) {
    if ($zombiePlayerId !== null) {
      self::checkAction('actionPlay');
    }
    $playerId = $zombiePlayerId ?? self::getActivePlayerId();
    // Do something
  }

The rest of the function can remain unchanged. It is important that your action.php does not pass in a value for $zombiePlayerId (for reasons that should be fairly obvious). It doesn't actually hurt to put this parameter onto all action handlers, whether or not the zombie will use them.

zombieTurn: The other half of the equation

Now that we've added $zombiePlayerId to our action handlers, we need our zombieTurn function to actually use it. This is as simple as adding the player ID to any actions called. (You are calling the action handlers instead of manually messing with the game state, right?)

Once you've done this, you're good to go. You can invoke callZombie(10) in your in-game chat and see ten zombie turns go by. You can invoke callZombie(9999) and maybe play a complete game. Because the activePlayerList will be empty when the game is in an end state, extra cycles won't have any adverse effect other than flooding your log.

But wait, there's more

Let's discuss how to make our test more useful. At bare minimum, by framework imperative, zombieTurn needs to make sure the zombie is no longer active in the current game state by the end of the function (although we may transition back to the same state with the same player active). Your exact needs will vary by game, but this often just results in a state transition with no action. That's fine, and it may even be the best approach in your game, but if we want to give our game an end-to-end test, we want to do something that advances the game toward an end state. If you're lucky, your game will naturally end after enough turns have passed (e.g. Reversi will end when the board is filled). In other cases, it's desirable to give zombies inert moves that don't advance game progression (e.g. Beyond the Sun only ends when enough achievements have been claimed).

When you're in the latter scenario, what you really want is zombies... with brains!!

The thinking man's zombie

We don't really want to modify zombieTurn, because actual, real games depend on that behavior. But we don't have to use zombieTurn in callZombie. We can duplicate zombieTurn into smartZombieTurn and call smartZombieTurn from callZombie.

The framework doesn't intend for your zombies to have any actual AI. Indeed, even your smart zombies can be fairly stupid as long as they're taking actions that are both legal and useful. If you have any functions that return a list of legal moves, you can leverage those for your smart zombie to pick from at random. For certain games, picking moves that advance the game can be challenging. If this describes your game, you may have to content yourself with a partial test, since in extreme cases, a more thorough approach is going to take longer than manual testing via the UI.

On the other hand, you can go down a rabbit hole with this and write zombie routines that give you full code coverage. Knock yourself out if that's what you want to do, but even in the worst case, our naive zombieTurn test represents a significant improvement in test automation.