API
Elements on this page refer to the file structure, concepts, and life cycle of an Empirica experiment.
The
gameInit
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) gameInit callback is required for Empirica to work.
The callback receives one argument, the
game
object, 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:name
: the name used to identify this stage in the UI codedisplayName
: which will be showed to the UI to playersdurationInSeconds
: 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.
Empirica.gameInit(game => {
game.players.forEach((player, i) => {
player.set("avatar", `/avatars/jdenticon/${player._id}`);
player.set("score", 0);
});
_.times(10, i => {
const round = game.addRound();
round.addStage({
name: "response",
displayName: "Response",
durationInSeconds: 120
});
if (game.treatment.playerCount > 1) {
round.addStage({
name: "response",
displayName: "Response",
durationInSeconds: 120
});
}
});
});
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.
onGameStart
is triggered once per game, before the game starts, and before the first onRoundStart
. It receives the game
object. Contrary to gameInit
, the Game has been created at this point.Empirica.onGameStart(game => {
if (game.treatment.myFactor === "fourtytwo") {
game.set("maxScore", 100);
} else {
game.set("maxScore", 0);
}
});
onRoundStart
is triggered before each round starts, and before onStageStart
. It receives the same options as onGameStart
, and the round that is starting.Empirica.onRoundStart((game, round) => {
round.set("scoreToReach", game.get("maxScore"));
});
onRoundStart
is triggered before each stage starts. It receives the same options as onRoundStart
, and the stage that is starting.Empirica.onStageStart((game, round, stage) => {
stage.set("randomColor", myRandomColorGenerator());
});
Empirica.onStageEnd((game, round, stage) => {
stage.set("scoreGroup", stage.get("score") > 10 ? "great" : "not_great");
});
Empirica.onRoundEnd((game, round) => {
let maxScore = 0;
game.players.forEach(player => {
const playerScore = player.round.get("score") || 0;
if (playerScore > maxScore) {
maxScore = playerScore;
}
});
round.set("maxScore", maxScore);
});
Empirica.onGameEnd(game => {
let maxScore = 0;
game.rounds.forEach(round => {
const roundMaxScore = round.get("maxScore") || 0;
if (roundMaxScore > maxScore) {
maxScore = roundMaxScore;
}
});
game.set("maxScore", maxScore);
});
It is very useful to be able to react to each update a user makes. Try nontheless to limit the amount of computations and database saves done in these callbacks. You can also try to limit the amount of calls to
set()
and append()
you make (avoid calling them on a continuous drag of a slider for example) and inside these callbacks use the key
argument at the very beginning of the callback to filter out which keys your need to run logic against.If you are not using these callbacks, comment them out, so the system does not call them for nothing.
onSet
is called when the experiment code call the .set()
method on games, rounds, stages, players, playerRounds or playerStages.Empirica.onSet((
game,
round,
stage,
player, // Player who made the change
target, // Object on which the change was made (eg. player.set() => player)
targetType, // Type of object on which the change was made (eg. player.set() => "player")
key, // Key of changed value (e.g. player.set("score", 1) => "score")
value, // New value
prevValue // Previous value
) => {
// Example filtering
if (key !== "value") {
return;
}
// Do some important calculation
});
onSet
is called when the experiment code call the .append()
method on games, rounds, stages, players, playerRounds or playerStages.Empirica.onAppend((
game,
round,
stage,
player, // Player who made the change
target, // Object on which the change was made (eg. player.set() => player)
targetType, // Type of object on which the change was made (eg. player.set() => "player")
key, // Key of changed value (e.g. player.set("score", 1) => "score")
value, // New value
prevValue // Previous value
) => {
// Note: `value` is the single last value (e.g 0.2), while `prevValue` will
// be an array of the previsous values (e.g. [0.3, 0.4, 0.65]).
});
onChange
is called when the experiment code call the .set()
or the .append()
method on games, rounds, stages, players, playerRounds or playerStages.onChange
is useful to run server-side logic for any user interaction. Note the extra isAppend
boolean that will allow to differenciate sets and appends.Empirica.onChange((
game,
round,
stage,
player, // Player who made the change
target, // Object on which the change was made (eg. player.set() => player)
targetType, // Type of object on which the change was made (eg. player.set() => "player")
key, // Key of changed value (e.g. player.set("score", 1) => "score")
value, // New value
prevValue, // Previous value
isAppend // True if the change was an append, false if it was a set
) => {
Game.set("lastChangeAt", new Date().toString());
});
onSubmit
is called when the experiment code call the .submit()
on a Stage.Note that onSubmit is only called if
.submit()
is explicitely called on the Stage object. Players for which the stage times out naturally, onSubmit
will not be triggered.Empirica.onSubmit((game, round, stage, player) => {
stage.set("lastSubmitAt", new Date().toString());
});
Adding bots to a game is as simple as defining a few callbacks. You can add different bots with different behaviors.
The
bot
method allows to add a bot with name
(e.g. "Alice"), while the configuration
is a set of callbacks that will allow to configure how the bot is suppose to react in certain conditions.The
configuration
has the follows callbacks:onStageTick
: called during each stage at 1 second intervalonStageStart
: CURRENTLY NOT SUPPORTED called at the beginning of eachstage (afteronRoundStart
/onStageStart
)onStageEnd
: CURRENTLY NOT SUPPORTED called at the end of each stage(afteronStageEnd
, beforeonRoundEnd
if it's the enf of the round)onPlayerChange
: CURRENTLY NOT SUPPORTED called each time any (human)player has changed a value
All callbacks are called with the following arguments:
secondsRemaining
: the number of remaining seconds in the stage
Empirica.bot("bob", {
onStageTick(bot, game, round, stage, secondsRemaining) {
let score = 0;
game.players.forEach(player => {
if (player === bot) {
return;
}
const playerScore = player.get("score");
if (playerScore > score) {
score = playerScore
}
});
bot.set("score", score+1);
}
};)
Set the
Round
Component that will contain all of the UI logic for your game.Component will receive the following props:
const Round = ({ player, game, round, stage }) => (
<div className="round">
<div className="profile">{player.id}: {player.get("score")}</p>
<div className="stimulus">{stage.get("somePieceOfData...")</p>
// ... Add round logic here. This is not a good example, we recommend you
// take a look a Tutorial or a Demo app for better examples.
</div>
);
Empirica.round(Round);
Optionally set the
Consent
Component you want to present players before they are allowed to register.Component will receive the following props:
Prop | Type | Description |
onConsent | Function | A function to call when the user has given consent (usually, clicked a "I consent" button). |
const Consent = ({ onConsent }) => (
<div className="consent">
<p>This experiment is part of...</p>
<p>
<button onClick={onConsent}>I CONSENT</button>
</p>
</div>
);
Empirica.consent(Consent);
Set the intro steps to present to the user after consent and registration, and before they are allowed into the Lobby. These steps might be instructions, a quiz/test, a survey... You may present the steps in multiple pages.
The
introSteps
callback should return an array of 0 or more React Components to show the user in order.Component will receive the following props:
N.B.: The
game
given here only has the treatment
field defined as the game has not yet been created.Empirica.introSteps((game, player) => {
const steps = [InstructionStepOne];
if (game.treatment.playerCount > 1) {
steps.push(InstructionStepTwo);
}
steps.push(Quiz);
return steps;
});
N.B.:
InstructionStepOne
or Quiz
, in this example, are components that are not implemented in this example, they are simply React Components.Set the exit steps to present to the user after the game has finished successfully (all rounds finished) or not (lobby timeout, cancelled game,...)
The
exitSteps
callback should return an array of 1 or more React Components to show the user in order.Component will receive the following props:
Prop | Type | Description |
game |