/**
* @module exp-player
* @submodule randomizers
*/
/**
* Randomizer to implement flexible condition assignment and counterbalancing by
* allowing the user to specify an arbitrary sequence of frames to create. A
* set of parameters is randomly selected from a list of available parameterSets,
* and these parameters are substituted in to the parameters specified in the
* list of frames.
*
* To use, define a frame with "kind": "choice" and "sampler": "random-parameter-set",
* as shown below, in addition to the parameters described under 'properties'.
*
* This
*
```json
"frames": {
"test-trials": {
"sampler": "random-parameter-set",
"kind": "choice",
"id": "test-trials",
"commonFrameProperties": {
"kind": "exp-lookit-story-page",
"baseDir": "https://s3.amazonaws.com/lookitcontents/ingroupobligations/",
"audioTypes": ["mp3", "ogg"],
"doRecording": true,
"autoProceed": false,
"parentTextBlock": {
"title": "Parents!",
"text": "Common instructions across test trials here",
"emph": true
}
},
"frameList": [
{
"images": [
{
"id": "agent",
"src": "AGENTIMG1",
"left": "40",
"width": "20",
"top": "10"
},
{
"id": "left",
"src": "LEFTIMG1",
"left": "10",
"width": "20",
"top": "50"
},
{
"id": "right",
"src": "RIGHTIMG1",
"left": "70",
"width": "20",
"top": "50"
}
],
"audioSources": [
{
"audioId": "questionaudio",
"sources": [{"stub": "QUESTION1AUDIO"}],
"highlights": "QUESTION1HIGHLIGHTS"
}
]
},
{
"images": [
{
"id": "agent",
"src": "AGENTIMG2",
"left": "40",
"width": "20",
"top": "10"
},
{
"id": "left",
"src": "LEFTIMG2",
"left": "10",
"width": "20",
"top": "50"
},
{
"id": "right",
"src": "RIGHTIMG2",
"left": "70",
"width": "20",
"top": "50"
}
],
"audioSources": [
{
"audioId": "questionaudio",
"sources": [{"stub": "QUESTION2AUDIO"}],
"highlights": "QUESTION2HIGHLIGHTS"
}
]
}
],
"parameterSets": [
{
"AGENTIMG1": "flurpagent1.jpg",
"LEFTIMG1": "flurpvictim1.jpg",
"RIGHTIMG1": "zazzvictim1.jpg",
"QUESTION1AUDIO": "flurpleftmean1",
"QUESTION1HIGHLIGHTS": [
{"range": [0.399293, 3.617124], "image": "agent"},
{"range": [5.085112, 6.811467], "image": "left"},
{"range": [6.905418, 8.702236], "image": "right"}
],
"AGENTIMG2": "flurpagent2.jpg",
"LEFTIMG2": "flurpvictim2.jpg",
"RIGHTIMG2": "zazzvictim2.jpg",
"QUESTION2AUDIO": "flurpleftinduct1",
"QUESTION2HIGHLIGHTS": [
{"range": [0.372569, 5.309110], "image": "agent"},
{"range": [5.495395, 7.209213], "image": "left"},
{"range": [5.495395, 7.209213], "image": "right"},
{"range": [9.966225, 11.922212], "image": "left"},
{"range": [12.052612, 14.008600], "image": "right"}
]
},
{
"AGENTIMG1": "zazzagent1.jpg",
"LEFTIMG1": "flurpvictim1.jpg",
"RIGHTIMG1": "zazzvictim1.jpg",
"QUESTION1AUDIO": "zazzrightnice1",
"QUESTION1HIGHLIGHTS": [
{"range": [0.348454, 3.736871], "image": "agent"},
{"range": [5.395033, 6.884975], "image": "left"},
{"range": [6.969085, 8.975701], "image": "right"}
],
"AGENTIMG2": "zazzagent2.jpg",
"LEFTIMG2": "flurpvictim2.jpg",
"RIGHTIMG2": "zazzvictim2.jpg",
"QUESTION2AUDIO": "zazzrightinduct1",
"QUESTION2HIGHLIGHTS": [
{"range": [0.572920, 5.138376], "image": "agent"},
{"range": [5.335317, 7.089884], "image": "left"},
{"range": [5.335317, 7.089884], "image": "right"},
{"range": [9.721735, 11.565821], "image": "left"},
{"range": [11.655340, 13.535233], "image": "right"}
]
}
],
"parameterSetWeights": [1, 1]
}
}
* ```
* @class randomParameterSet
*/
function getRandomElement(arr, weights) {
var totalProb = weights.reduce((a, b) => a + b, 0);
var randPos = Math.random() * totalProb;
var weightSum = 0;
for (var i = 0; i < arr.length; i++) {
weightSum += weights[i];
if (randPos <= weightSum) {
return [i, arr[i]];
}
}
}
function replaceValues(obj, rep) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
if (typeof obj[property] === 'object') {
obj[property] = replaceValues(obj[property], rep);
} else {
if (rep.hasOwnProperty(obj[property])) {
obj[property] = rep[obj[property]];
}
}
}
}
return obj;
}
var randomizer = function(frameId, frameConfig, pastSessions, resolveFrame) {
// Data provided to randomizer (properties of frameConfig):
/**
* Object describing common parameters to use in EVERY frame created
* by this randomizer. Parameter names and values are as described in
* the documentation for the frameType used.
*
* @property {Object} commonFrameProperties
*/
/**
* Unique string identifying this set of frames
*
* @property {String} id
*/
/**
* List of frames to be created by this randomizer. Each frame is an
* object with any necessary frame-specific properties specified. The
* 'kind' of frame can be specified either here (per frame) or in
* commonFrameProperties. If a property is defined for a given frame both
* in this frame list and in commonFrameProperties, the value in the frame
* list will take precedence.
*
* (E.g., you could include 'kind': 'normal-frame' in
* commmonFrameProperties, but for a single frame in frameList, include
* 'kind': 'special-frame'.)
*
* Any property VALUES within any of the frames in this list which match
* a property NAME in the selected parameterSet will be replaced by the
* corresponding parameterSet value. E.g., suppose a frame in frameList is
*
* ```
* {'leftImage': 'LEFTIMAGE1',
* 'rightImage': 'frog.jpg',
* 'size': 'IMAGESIZE'}
* ```
*
* and the row that has been selected randomly of parameterSets is
*
* ```
* {'LEFTIMAGE1': 'toad.jpg',
'LEFTIMAGE2': 'dog.jpg',
'IMAGESIZE': 250}
* ```
*
* Then the frame would be transformed into:
* ```
* {'leftImage': 'toad.jpg',
* 'rightImage': 'frog.jpg',
* 'size': 250}
* ```
*
* The same values may be applied across multiple frames. For instance,
* suppose frameList is
```
[
{
'leftImage': 'LEFTIMAGE1',
'rightImage': 'frog.jpg',
'size': 'IMAGESIZE'
},
{
'leftImage': 'LEFTIMAGE2',
'rightImage': 'frog.jpg',
'size': 'IMAGESIZE'
}
]
```
* Then the corresponding processed frames would include the values
```
[
{
'leftImage': 'toad.jpg',
'rightImage': 'frog.jpg',
'size': 250
},
{
'leftImage': 'dog.jpg',
'rightImage': 'frog.jpg',
'size': 250
}
]
```
* A property value like 'IMAGESIZE' may be placed in a frame definition
* nested within another object (at any depth) or within a list and
* will still be replaced.
* @property {Object[]} frameList
*/
/**
* Array of parameter sets to randomly select from in order to determine
* the parameters for each frame in this session.
*
* A single element of parameterSets will be applied to a given session.
*
* @property {Object[]} parameterSets
*/
/**
* [Optional] Array of weights for parameter sets; elements correspond to
* elements of parameterSets. The probability of selecting an element
* parameterSets[i] is parameterSetWeights[i]/sum(parameterSetWeights).
*
* If not provided, all parameterSets are weighted equally.
*
* This is intended to allow manual control of counterbalancing during
* data collection, e.g. to allow one condition to "catch up" if it was
* randomly selected less often.
*
* @property {Number[]} parameterSetWeights
*/
// Select a parameter set to use for this trial.
if (!(frameConfig.hasOwnProperty('parameterSetWeights'))) {
frameConfig.parameterSetWeights = new Array(frameConfig.parameterSets.length).fill(1);
}
var parameterData = getRandomElement(frameConfig.parameterSets, frameConfig.parameterSetWeights);
var parameterSetIndex = parameterData[0];
var parameterSet = parameterData[1];
var frames = [];
var thisFrame = {};
for (var iFrame = 0; iFrame < frameConfig.frameList.length; iFrame++) {
// Assign parameters common to all frames made by this randomizer
thisFrame = {};
Object.assign(thisFrame, frameConfig.commonFrameProperties);
// Assign parameters specific to this frame (allow to override
// common parameters assigned above)
Object.assign(thisFrame, frameConfig.frameList[iFrame]);
// Substitute any properties that can be replaced based on
// the parameter set.
thisFrame = replaceValues(thisFrame, parameterSet);
// Assign frame ID
thisFrame.id = `${frameId}`;
thisFrame = resolveFrame(thisFrame.id, thisFrame)[0];
frames.push(...thisFrame); // spread syntax important here -- a list of frames is returned by resolveFrame.
}
return [frames, {'conditionNum': parameterSetIndex, 'parameterSet': parameterSet}];
};
export default randomizer;
// Export helper functions to support unit testing
export { getRandomElement, replaceValues};