Skip to content

StatsSystemDeveloperInfo

davewx7 edited this page Mar 12, 2012 · 1 revision

The Frogatto stats system allows sending of stats events, or samples, generated while users are playing Frogatto to the Frogatto Stats Server. The Frogatto Stats Server aggregates these events according to certain rules and makes the data available using a web-based API.

An event consists of an FFL map object, which must have a 'type' attribute, and may contain any arbitrary set of other attributes. Firing a stats event using FFL is easy:

report({'type' -> 'myevent', value -> 5, other_values -> [8,4,5]})

Note that the ID of the level that the player was in when the event was fired is automatically tracked and tied to the event.

Certain events are sent by the game engine itself. These events include:

{'move', 'x' -> 8, 'y' -> 12}

Move events -- the game engine regularly samples the location of the player and transmits events with their location.

{'quit', 'x' -> 43, 'y' -> 18} {'die', 'x' -> 72, 'y' -> 38}

Quit and Die events -- when the player exits the game or dies, these events are sent with the location they were at.

{'load', time -> 16}

Load events -- When the game engine loads a level it transmits this event, with the amount of time (in milliseconds) it took to load.

When the player quits the game, the game engine will transmit all events recorded during the play session to the server. Some additional data is sent, including:

  • Segmentation of the events into the levels in which they occurred.
  • A unique ID associated with the player (or more correctly the Frogatto install) which generated the events.
  • The version of Frogatto under which the events were generated.

The Frogatto Stats Server records events segregated in several ways:

  • A separate data set for each version of Frogatto, as well as a 'global' data set which includes data aggregated from all versions.
  • A separate data set for each level. Some types of data will also record their data aggregated across all levels.
  • By the type of the data events.

The server's rules.json configuration file specifies tables for each type of event, with rules of how to aggregate received events into these tables.

A table consists of a mapping of arbitrary FFL keys to arbitrary FFL values. Let's take a look at a simple definition of a table to handle quit events:

{
  "name": "quit",
  "tables": [
    {
      "name": "by_location",
      "key": "[x, y]",
      "value": "value + 1",
      "init_value": "0",
      "global_scope": false
    }
  ]
}

Note the key parts of this definition:

  • There is one table defined for 'quit' events, which is called 'by_location' (since it tracks quit events by the location they occur at)
  • The key attribute calculates the key in the table that the new event will be stored at.
  • The init_value attribute calculates the value that will be assigned to a freshly created key in the table.
  • The value attribute calculates the new value of the entry. It has access to 'value', the current value, and 'sample', the event that has arrived.
  • The global_scope attribute here specifies not to use this table across all levels. It only makes sense within the context of a level.
  • Note that in this example, value and init_value have the default behavior and so wouldn't need to be specified.

Let us take an example of what occurs when some events arrive:

EVENT: { 'type' -> 'quit', 'x' -> 8, 'y' -> 16}

The table will be in the following state:

[
  {
    "key": [8, 16],
    "value": 1
  }
]

Another event arrives:

EVENT: { 'type' -> 'quit', 'x' -> 28, 'y' -> 13}

Now the table looks like this:

[
  {
    "key": [8, 16],
    "value": 1
  },
  {
    "key": [28, 13],
    "value": 1
  }
]

We can see that after receiving two events we have two entries in the table. But, suppose we received another event with the same location as a previous event:

EVENT: { 'type' -> 'quit', 'x' -> 8, 'y' -> 16}

The key generated would be the same as an existing key in the table, so the table would have two entries but with an updated value:

[
  {
    "key": [8, 16],
    "value": 2
  },
  {
    "key": [28, 13],
    "value": 1
  }
]

Note that a goal of the Frogatto stats system is to take the large amount of data that is received and aggregate it into tables which are much smaller and thus can be efficiently accessed. In the case of location-based messages, such as the quit message, a table keyed by location is likely to end up with many distinct keys and thus many entries. This is undesirable. However, usually we don't need to know the exact location a player quit at, so we can reduce the keyspace dramatically by rounding it. We could re-write the key calculation like this:

"key": "[(x/32)*32 + 16, (y/32)*32 + 16]"

This maps the location to the center of the 32x32 tile it is in. This greatly reduces the number of possible keys.

As another example, we want to know the average (mean) time it takes for players to load levels. For this, we don't need to use distinct keys at all -- we just want a table with a single value which holds the mean. Here is our table definition:

		"name": "load",
		"tables": [
			{
				"name": "average_load_time",
				"value": "{ 'nsamples' -> (value.nsamples+1), 'sum' -> value.sum + sample.time, 'mean' -> (value.sum+sample.time)/(value.nsamples+1) }",
				"init_value": {
					"sum": 0,
					"nsamples": 0,
					"mean": 0
				}
			}

Note that to calculate the average load time, we need to keep track of the number of entries we've seen, and the sum of them. The value keeps track of these numbers, and uses them to calculate the average every time a new event arrives.

Suppose we had several load events arrive:

{'type' -> 'load', 'time': 50}
{'type' -> 'load', 'time': 20}
{'type' -> 'load', 'time': 10}

Now the table would look like this:

[
  {
    "key": null,
    "value: {
      "sum": 80,
      "nsamples": 3,
      "mean": 26
    }
  }
]

Unique Users

When calculating keys, we also have access to the user_id field, which is an integer that is unique per Frogatto install. This allows us to track the number of unique users that have triggered different event conditions. For instance, we can add a table definition for the load event to easily track the number of players that have reached various levels:

			{
				"name": "unique_users",
				"key": "user_id"
			}

This will create a table that looks like this:

      {
        "entries": [
          {
            "key": 482375297,
            "value": 1
          },
          {
            "key": 985993827,
            "value": 2
          },
          {
            "key": 432009403,
            "value": 1
          },
        ],
        "name": "unique_users"
      }

Using this data we can conveniently track the number of users that have reached each different level in the game (and how many times they have done so).

Accessing the Data

To access the data received from the stats system we access a URL such as this:

http://theargentlark.com:5000/?version=1.2&level=titlescreen.cfg

This will give us all the stats data aggregated for titlescreen.cfg for version 1.2 of Frogatto.

Here is an annotated sample response we might get:

[ <-- A list of all the types of events.
  { <-- begin the entry for the load event.
    "tables": [ <-- a list of all the tables that are associated with this type of event.
      { <-- beginning of the 'sum' table.
        "entries": [ <-- the entries in the table.
          {
            "key": null,
            "value": {
              "mean": 27, <-- the mean of all load times.
              "nsamples": 6,
              "sum": 162
            }
          }
        ],
        "name": "sum" <-- name of the table.
      }, <-- end of the 'sum' table.
      { <-- beginning of the unique_users table.
        "entries": [ <-- entries in the table.
          {
            "key": 482375297,
            "value": 2
          },
          {
            "key": 2120124826,
            "value": 1
          }
        ],
        "name": "unique_users" <-- name of the table.
      } <-- end of the unique_users table
    ], <-- end of the list of tables associated with the event.
    "total": 6,  <-- the number of occurrences of this event we've seen.
    "type": "load"  <-- type of the event.
  }, <-- end the entry for the load event
  {
    "tables": [
      {
        "entries": [
          {
            "key": [
              400,
              432
            ],
            "value": 5
          },
          {
            "key": [
              432,
              432
            ],
            "value": 31
          },
          {
            "key": [
              464,
              432
            ],
            "value": 1
          },
          {
            "key": [
              496,
              432
            ],
            "value": 1
          },
          {
            "key": [
              560,
              432
            ],
            "value": 1
          },
          {
            "key": [
              592,
              400
            ],
            "value": 1
          },
          {
            "key": [
              624,
              240
            ],
            "value": 1
          },
          {
            "key": [
              624,
              336
            ],
            "value": 1
          },
          {
            "key": [
              656,
              176
            ],
            "value": 1
          },
          {
            "key": [
              656,
              336
            ],
            "value": 1
          },
          {
            "key": [
              688,
              176
            ],
            "value": 1
          },
          {
            "key": [
              752,
              240
            ],
            "value": 1
          }
        ],
        "name": "tile_group"
      }
    ],
    "total": 46,
    "type": "move"
  },
  {
    "tables": [],
    "total": 2,
    "type": "quit"
  }
]

Clone this wiki locally