Game State Version 3

- 3 mins read

When I attempt to run the application in the dev environment, an existing game session stored in my local dev database is crashing after it resumes and attempts to process move actions. After some debugging, I find that this is due to the events log being in a bad state caused by the recent change of adding the :events_visible_by_player field. I never added a new test case for upgrading the game state version after making this change and now I’m paying for it.

The map value of :events_visible_by_player is expected to always be populated with a list for each player id in the game. When a new game state is initialized, these lists are created automatically. However, when upgrading from version 1 game state which does not have an events log, the game state is updated with EventsLog.new() which sets an empty map for :events_visible_by_player.

defmodule Minotaur.GameEngine.Game do
  # …

  def upgrade_latest_version(%{version: 1} = game) do
    v2 = %{game | events_log: EventsLog.new(), version: 2}

    upgrade_latest_version(v2)
  end
end

To fix this issue, I’ll need to define a new upgrade path to the latest version which will be bumped to 3. I start by writing a new test case for the desired behavior for the scenario of calling upgrade_latest_version with a version 2 game state.

defmodule Minotaur.GameEngine.GameTest do
  # …

  describe "upgrade_latest_version/1 from version 2" do
    setup [:create_game, :rollback_v3]

    test "returns latest version state", %{game: game} do
      game = Game.upgrade_latest_version(game)

      assert Game.latest_version() == game.version
      assert %EventsLog{events_visible_by_player: player_events} = game.events_log

      Enum.each(game.players, fn {player_id, _} ->
        assert [] == player_events[player_id]
      end)
    end
  end

  # v3 sets default player events as empty lists
  def rollback_v3(%{game: game}) do
    game =
      game
      |> put_in([:events_log, :events_visible_by_player], %{})
      |> Map.put(:version, 2)

    [game: game]
  end

  defp create_game(_ctx) do
    [game: game_fixture()]
  end
end

I then increment the current version module attribute and add a new function definition to handle upgrading from version 2 game state.

defmodule Minotaur.GameEngine.Game do
  # …

  @current_version 3

  # …

  def upgrade_latest_version(%{version: @current_version} = game) do
    game
  end

  def upgrade_latest_version(%{version: 2} = game) do
    events_log = EventsLog.new(events: game.events_log.events, players: game.players)

    v3 =
      game
      |> Map.put(:events_log, events_log)
      |> Map.put(:version, 3)

    upgrade_latest_version(v3)
  end

  def upgrade_latest_version(%{version: 1} = game) do
    v2 = %{game | events_log: EventsLog.new(), version: 2}

    upgrade_latest_version(v2)
  end

  def upgrade_latest_version(_) do
    {:error, :invalid_game_version}
  end
end

The test passes so I launch the application again in the dev environment. The game session resumes and is able to process moves correctly after the game state is automatically upgraded.

The next feature I’ll focus on is to render the events log for each player in their game UI.