LogoLogo
  • Introduction
  • Getting Started
    • Setup
      • Windows WSL Instructions (new)
      • Windows WSL Instructions
      • Linux Instructions
    • Creating your experiment
    • Running your experiment
    • Updating your experiment
  • Conceptual Overview
    • Game Life Cycle
      • Customising when players submit stages
    • Concepts
    • Randomization & Batches
    • API
  • Guides
    • V2 Migration
    • Managing the Data
    • Special Empirica Components
    • The Admin Panel
    • Deploying Your Experiment
      • Ubuntu tutorial
  • Tutorials
    • Beginner Experiment: Prisoner's Dilemma
      • Part 1: Before you start
      • Part 2: Creating the Experiment in Empirica
      • Part 3: Getting Accustomed to the Code
      • Part 4: Coding the Prisoner's Dilemma Game
        • Part 4.1: Removing example code
        • Part 4.2: Intro Text
        • Part 4.3: Set up the game stages
        • Part 4.4: Build the "Choice" React Component
        • Part 4.5: Build the "Result" React Component
        • Part 4.6: Compute the Score
      • Part 5: Customizing the experiment
        • Part 5.1: Changing the number of rounds
        • Part 5.2: Turning the chat on and off
      • Part 6: Deployment
  • FAQ
    • I need help!
    • The Processes and Elements of an Empirica Experiment
    • Managing Players and Games
  • Resources
    • Helpful Linux Commands
    • Code Editors
    • Javascript and React
  • Links
    • Empirica website
    • Twitter
    • GitHub
Powered by GitBook
On this page
  • Server
  • Empirica.onGameStart(callback)
  • Game Callbacks
  • Empirica.onRoundStart(callback)
  • Empirica.onStageStart(callback)
  • Empirica.onStageEnded(callback)
  • Empirica.onRoundEnded(callback)
  • Empirica.onGameEnded(callback)
  • Empirica.on(model, callback)
  • Empirica.on(model, attributeName, callback)
  • Empirica.flush()
  • Server Objects
  • Game object
  • Round object
  • Stage object
  • Player object
  • Client

Was this helpful?

Edit on GitHub
Export as PDF
  1. Conceptual Overview

API

PreviousRandomization & BatchesNextV2 Migration

Last updated 11 months ago

Was this helpful?

This document describes Empirica's , and APIs.

Server

Empirica.onGameStart(callback)

The onGameStart callback is called just before a game starts, when all players are ready, and it must create rounds and stages for the game.

One (and one only) onGameStart callback is required for Empirica to work.

The callback receives one argument, the , which gives access to the players and the treatment for this game.

It also offers the addRound() method, which allows to add a round to the game. The returned Round object will implement the addStage(stageArgs) method, which allows to add a Stage to the Round. The stageArgs object to be passed to the stage creation method must contain:

  • duration: the stage duration, in seconds

Note that the Game has not yet been created when the callback is called, and you do not have access to the other properties of the Game which will be created subsequently.

Example

Empirica.gameInit(game => {
  game.players.forEach((player, i) => {
    player.set("score", 0);
  });

  const round1 = game.addRound();
  round1.addStage({
    duration: 120
    name: "Response",
  });

  if (game.treatment.playerCount > 1) {
    round1.addStage({
      duration: 120
      name: "Result",
    });
  }

  const round2 = game.addRound();
  round1.addStage({
    duration: 300
    name: "Response",
    someotherfield: "mydata"
  });

  if (game.treatment.playerCount > 1) {
    round1.addStage({
      duration: 120
      name: "Result",
    });
  }
});

Game Callbacks

Game hooks are optional methods attached to various events throughout the game life cycle to update data on the server-side.

Contrary to client side data updates, sever-side updates are synchronous, there is no risk of conflicting updates, and important calculations can be taken at precise points along the game.

``

Empirica.onRoundStart(callback)

Example

Empirica.onRoundStart(({ round }) => {
  round.set("scoreToReach", round.currentGame.get("maxScore"));
});

Empirica.onStageStart(callback)

Example

Empirica.onStageStart(({ stage }) => {
  stage.set("randomColor", myRandomColorGenerator());
});

Empirica.onStageEnded(callback)

Example

Empirica.onStageEnded(({ stage }) => {
  const expectedScore = stage.round.get("expectedScore");
  const group = stage.get("score") > expectedScore ? "great" : "not_great";
  stage.set("scoreGroup", group);
});

Empirica.onRoundEnded(callback)

Example

Empirica.onRoundEnded(({ round }) => {
  let maxScore = 0;
  round.currentGame.players.forEach((player) => {
    const playerScore = player.round.get("score") || 0;
    if (playerScore > maxScore) {
      maxScore = playerScore;
    }
  });
  round.set("maxScore", maxScore);
});

Empirica.onGameEnded(callback)

onGameEnded is triggered when the game ends. It receives the game that just ended.

Example

Empirica.onGameEnded(({ game }) => {
  let maxScore = 0;
  game.rounds.forEach((round) => {
    const roundMaxScore = round.get("maxScore") || 0;
    if (roundMaxScore > maxScore) {
      maxScore = roundMaxScore;
    }
  });
  game.set("maxScore", maxScore);
});

Empirica.on(model, callback)

on(model, callback) listens to the creation of new mode objects. Model objects are the "game", "round", "stage", "player" and "batch". The callback receives a ctx object, and the model object listened to. In the example below, we're listening to new games, so we receive the game as argument.

Empirica.on("game", (ctx, { game }) => {
  if (game.get("initCalc")) return;
  
  game.set("initCalc", initCalcs());
});

Beware, Empirica.on(model, callback)is called on new objects and when the server restarts (when you run empirica or the server restarts after a code change in development mode). You should add a check such as the one in the example above, where we check if initCalc was already set at the top of the callback.

Empirica.on(model, attributeName, callback)

on(model, attributeName, callback) listens on changes to the attribute of attributeName on model. Model objects are the "game", "round", "stage", "player" and "batch". The callback receives a ctx object, the model object, and the attribute value. In the example below, we're listening on changes to choice on the player model.

Empirica.on("player", "choice", (ctx, { player, choice }) => {
  if (choice === "yes") {
    // ...
  }
});

We recommend using the value of the attribute given on the callback argument instead of doing .get(attributeName) on the model object as the value could have changed changed asynchronously.

Empirica.flush()

Empirica.flush(): Promise<void> adds the ability to manually flush changes outside of callback functions. This is useful when you want to make changes asynchronously without blocking the callback. For example, if you want to call an external API and update an attribute with the results, but your don't want to block the callback while waiting for the API to respond.

Empirica.flush() will return a promise that resolves when the changes have been flushed. You can use await Empirica.flush() to wait for the changes to be flushed. But you do not have to wait for the flush. It is only useful if you have multiple flushes in a single function, where you want to commit the changes in steps.

The simplest example:

Empirica.on("stage", "myTrigger", (ctx, { stage, myTrigger }) => {
  callMyAPI.then((value) => {
    stage.set("value", value);
    Empirica.flush();
  });
});

An example with different use cases:

Empirica.on("stage", "myTrigger", (ctx, { stage, myTrigger }) => {
  // Copying the stage to the global namespace so we can use it outside of the
  // callback function. See below this callback function.
  myStage = stage;

  // flush
  setTimeout(() => {
    stage.set("b", (stage.get("b") || 0) + 1);
    Empirica.flush();
  }, 2000);
});

// Note: copying a stage to the global namaspace like this is not recommended,
// this is just for illustration purposes. If you have multiple games running
// at the same time, stages will overwrite each other.
let myStage;
setInterval(() => {
  if (myStage) {
    myStage.set("c", (myStage.get("c") || 0) + 1);
    Empirica.flush();
  }
}, 1000);

Note: if you perform blocking operations between changes, the changes before the blocking operations might be flushed before flushing manually. The change log is global, so while you're blocking, other changes might happen in a callback elsewhere, and that could trigger a flush of all latent changes. If you want to make a set of changes atomic, you should not do any blocking operations between them (i.e. use await or Promises) or collect your changes in a local datastructure and apply them at once.

If you are starting an interval (setInterval), you should make sure to clear it properly. Here's an example with an interval per game:

const timers = {};

// We use the game event to start the interval. this ensures that the interval
// is started on a server restart.
Empirica.on("game", (ctx, { game }) => {
  if (!game.isRunning) return;

  timers[game.id] ||= setInterval(() => {
    // do something
    Empirica.flush();
  }, 1000);
});

Empirica.onGameEnd(({ game }) => {
  clearInterval(timers[game.id]);
});

Or have a single interval that manages all games:

const activeGames = new Set();

// We use the game event to start the interval. this ensures that the interval
// is started on a server restart.
Empirica.on("game", (ctx, { game }) => {
  if (game.isRunning) {
    activeGames.add(game);
  }
});

setInterval(() => {
  for (const game of activeGames) {
    // do something
  }
  Empirica.flush();
}, 1000);

Empirica.onGameEnd(({ game }) => {
  activeGames.delete(game.id);
});

Server Objects

Game object

Property
Type
Description

players

Players participating in this Game.

rounds

This will return every round that makes up the game.

stages

This will return every stage that makes up the game.

currentRound

The current Round.

currentStage

The current Stage.

Round object

Property
Type
Description

stages

Stages composing this Round.

currentGame

Game this round is a part of.

Stage object

Property
Type
Description

round

Round this stage is a part of.

currentGame

Game this stage is a part of.

Player object

Property
Type
Description

id

String

The ID the player used to register (e.g. MTurk ID).

currentRound

Round the player is currently in.

currentStage

Stage the player is currently in.

currentGame

Game the player is currently in.

Client

onRoundStart is triggered before each round starts, and before onStageStart. It receives the same options as onGameStart, and the that is starting.

onRoundStart is triggered before each stage starts. It receives the same options as onRoundStart, and the that is starting.

onStageEnded is triggered after each stage. It receives the current , the current , and that just ended.

onRoundEnded is triggered after each round. It receives the current , and the that just ended.

Array of

Array of

Array of

Array of

See the page for more info.

Special Empirica Component
server
client
shared
game object
round
stage
game
round
stage
game
round
Player objects
Round objects
Stage objects
Round object
Stage object
Stage objects
Game object
Round object
Game object
Round object
Stage object
Game object