const game_config = {
timeout1Instructions: 10,
timeout2Scenario: 10,
timeout3Chat: 60,
timeout4Answer: 60,
distribution: {
hard: {
public: [0, 1],
private: [
[6, 2], // leader
[7, 3],
[8, 4],
[9, 5],
],
},
medium: {
public: [6, 1],
private: [
[2, 3], // leader
[7, 0],
[8, 4],
[9, 5],
],
},
easy: {
public: [6, 7],
private: [
[8, 9], // leader
[0, 3],
[1, 4],
[2, 5],
],
},
},
// must be a diagonally symetric matrix
// or one player will get the chatbox but the other one won't
// thus the communication would be impossible
chatAdjacency: [
[0, 1, 1, 1],
[1, 0, 0, 0],
[1, 0, 0, 0],
[1, 0, 0, 0],
],
chatName: ["Leader", "Green", "Blue", "Yellow"],
chatTextColor: ["#a94442", "#3c763d", "#31708f", "#8a6d3b"],
chatBackgroundColor: ["#f2dede", "#dff0d8", "#d9edf7", "#fcf8e3"],
chatBorderColor: ["#ebccd1", "#d6e9c6", "#bce8f1", "#faebcc"],
};
// will be populated by comReceived with com_type 0
// this is because roles and ids are set outside this experiment and may be different from ids within it
const players = [];
let params;
/**
* Main initialization script automatically called by the environment once all the data has been initialized
*/
function initialize() {
// set this player's properties from the initial state
player.initialize();
// things that are relevant only to the current player can be initialized immendiately
params = parameterHandler(variables);
const scenario = scenarios[params.scenario];
/*
if (params.version === AMBIGUOUS) {
scenario.culpritClues[7] = scenario.culpritClues[10];
scenario.targetClues[7] = scenario.targetClues[10];
scenario.dayClues[7] = scenario.dayClues[10];
}
*/
const distribution = game_config.distribution[params.infoDistribution];
render(params, scenario, distribution);
bindAnswerEvents(scenario);
// introduce yourself,
// once all players have introduced themselves initReady will trigger
sendCom(player.gid, 0);
timer
.queueEvent(0, showInstructions)
.queueEvent(game_config.timeout1Instructions, showScenario)
.queueEvent(game_config.timeout2Scenario, showAnswerAll)
.queueEvent(game_config.timeout4Answer, showChat)
.queueEvent(game_config.timeout3Chat, showAnswer)
.queueEvent(game_config.timeout4Answer, endGame);
// handle quitting
const _doQuit = doQuit;
window.doQuit = function () {
store.clear();
_doQuit();
};
}
/**
* process communication
*/
function comReceived(i, com) {
const to_parts = typeof com.to_parts.reduce !== "undefined" ? com.to_parts : [com.to_parts];
const forMe = to_parts.reduce((forMe, id) => forMe || +id === +myid, false);
if (+com.com_type === -1 && forMe) {
// this is a chat
chatReceived(com.from, com.val);
} else if (+com.com_type === 0) {
// player sending their gid
console.log(com.val);
players[+com.val] = +com.from;
introducedCount = players.reduce((count, o) => (typeof o !== "undefined" ? ++count : count), 0);
if (introducedCount >= numPlayers) {
initReady();
}
}
}
/**
* called after all relevant information has been shared among users
*/
function initReady() {
console.log("gid to id map", players);
bindChat();
timer.run();
}
/**
* fills in dynamic data into the page
*/
function render(params, scenario, distribution) {
if (player.isLeader) {
$(".leader").show();
$(".follower").hide();
} else {
$(".leader").hide();
$(".follower").show();
}
const timers = ["timeout1Instructions", "timeout2Scenario", "timeout3Chat", "timeout4Answer"];
for (const timer of timers) {
$(`[data-id='${timer}']`).text(game_config[timer]);
}
const dimensionName = ["culprit", "target", "day"];
for (let i = 1; i <= dimensionName.length; ++i) {
const dimension = dimensionName[i - 1];
const l = "ABCDE".split("");
$(`[data-id='scenarioDimension${i}']`).text(dimension);
for (let j = 0; j < scenario[`${dimension}Clues`].length; ++j) {
$(`[for='dim${i}${l[j]}']`).text(scenario[dimension][j]);
}
}
let clues = [
scenario.culpritClues[distribution.public[0]],
scenario.culpritClues[distribution.public[1]],
scenario.targetClues[distribution.public[0]],
scenario.targetClues[distribution.public[1]],
scenario.dayClues[distribution.public[0]],
scenario.dayClues[distribution.public[1]],
scenario.culpritClues[distribution.private[+player.gid][0]],
scenario.culpritClues[distribution.private[+player.gid][1]],
scenario.targetClues[distribution.private[+player.gid][0]],
scenario.targetClues[distribution.private[+player.gid][1]],
scenario.dayClues[distribution.private[+player.gid][0]],
scenario.dayClues[distribution.private[+player.gid][1]],
];
if (params.shuffleClueDisplay) {
clues = shuffleArray(clues);
}
let clueItems = "";
for (const clue of clues) {
clueItems += `
${clue}`;
}
const scenarioContent = `
${scenario.title}
${scenario.flavorText}
`;
$("[data-id='scenario']").html(scenarioContent);
if (params.repeatCluesDuringChat) {
$(".repeat-clues").show();
} else {
$(".repeat-clues").hide();
}
}
/**
*
*/
function bindAnswerEvents(scenario) {
for (let i = 1; i <= scenario.dimensions.length; ++i) {
const dimSum = $(`#dim${i}Sum`);
const dimInputs = $(`#dim${i}A,#dim${i}B,#dim${i}C,#dim${i}D,#dim${i}E`);
dimInputs.change((e) => {
let sum = 0;
dimInputs.each((i, o) => {
sum += +$(o).val();
});
if (sum === 99) {
sum = 100;
}
dimSum.text(`${sum}%`);
if (sum === 100) {
dimSum.removeClass("text-danger");
dimSum.addClass("text-success");
} else {
dimSum.addClass("text-danger");
dimSum.removeClass("text-success");
}
});
}
$("form#answers").submit((e) => {
e.preventDefault();
});
}
function resetAnswerForm() {
for (let i = 1; i <= 3; i++) {
const dimInputs = $(`#dim${i}A,#dim${i}B,#dim${i}C,#dim${i}D,#dim${i}E`);
dimInputs.val(0);
$(`#dim${i}Sum`).removeClass("text-success").addClass("text-danger").text("0%");
}
$("#confirmAnswers").off("click");
}
/**
*
*/
function bindChat() {
const navbar = $("#chat .nav-tabs");
const chats = $("#chat .tab-content");
const tabTemplate = $("li", navbar).detach();
const panelTemplate = $("#chat .tab-pane").detach();
const adjacency = game_config.chatAdjacency[+player.gid];
const adjacencyCount = adjacency.reduce((acc, val) => acc + +!!val, 0);
let first = true;
adjacency.map((connected, i) => {
if (connected) {
const playerId = players[i];
// const playerLabel = `participant${i + 1}`;
const playerLabel = game_config.chatName[i];
const tab = tabTemplate.clone();
tab.css({
"margin-bottom": "-4px",
});
navbar.append(tab);
const panel = panelTemplate.clone();
panel.attr({ id: playerLabel });
chats.append(panel);
// make sure the first item is selected
if (first) {
tab.addClass("active");
panel.addClass("active");
first = false;
}
// render tab and pane information
const tablink = $("a", tab);
tablink
.attr({
href: `#${playerLabel}`,
})
.text(playerLabel)
.click((e) => {
e.preventDefault();
$(this).tab("show");
tablink.removeClass("new");
});
panel.attr({ id: playerLabel });
// bind player chat
const $form = $("form", panel);
$form.submit((e) => {
e.preventDefault();
message = $(`input[type=text]`, $form).val();
if (message.trim() === "") {
return false;
}
sendChat(message, playerId);
$(`input[type=text]`, $form).val("");
$(".chatBox", panel).append(renderChatMessage(message, true, players.indexOf(+myid)));
const now = new Date();
store.addLog(
XML(
"chatMessage",
{
from: player.gid,
to: playerId,
time: now.toUTCString(),
},
message
)
);
submit(
XML(
"chatMessage",
{
from: player.gid,
to: playerId,
time: now.toUTCString(),
intermediateLog: true,
},
message
)
);
});
}
});
}
/**
*
*/
function chatReceived(from, message) {
if (+from !== +myid) {
const playerGid = players.indexOf(+from);
// const playerLabel = `participant${playerGid}`;
const playerLabel = game_config.chatName[playerGid];
const panel = $(`#${playerLabel} .chatBox`);
const tablink = $(`a[href="#${playerLabel}"]`);
if (!tablink.parent().hasClass("active")) {
tablink.addClass("new");
}
panel.append(renderChatMessage(message, false, playerGid));
panel.animate({ scrollTop: panel[0].scrollHeight }, 1000);
}
}
/**
*
*/
function renderChatMessage(message, isMe, from) {
const messageClass = isMe ? "alert alert-success pull-right" : "alert alert-info pull-left";
const nameTag = $("").text(`${isMe ? "Me" : game_config.chatName[+from]}: `);
return $("")
.addClass(messageClass)
.css({
clear: "both",
color: game_config.chatTextColor[+from],
"background-color": game_config.chatBackgroundColor[+from],
"border-color": game_config.chatBorderColor[+from],
})
.text(message)
.prepend(nameTag);
}
/**
*
*/
function showInstructions() {
$("#instructions").show();
}
function hideInstructions() {
$("#instructions").hide();
}
/**
*
*/
function showScenario() {
hideInstructions();
$("#scenario").show();
}
function hideScenario() {
$("#scenario").hide();
}
/**
*
*/
let answermode = 0;
let answersLockedIn = false;
function showAnswerAll() {
hideScenario();
resetAnswerForm();
answermode = 0;
$("#answerLeader").show();
$("#answerGuideAll").show();
$("#answerGuideLeader").hide();
$("#confirmAnswers").click((e) => {
answersLockedIn = true;
$("#answerLeader").hide();
$("#answerLocked").show();
});
}
function showAnswer() {
hideChat();
resetAnswerForm();
answermode = 1;
if (player.isLeader) {
$("#answerLeader").show();
$("#answerGuideAll").hide();
$("#answerGuideLeader").show();
$("#confirmAnswers").click((e) => {
answersLockedIn = true;
$("#answerLeader").hide();
$("#answerLocked").show();
});
} else {
$("#answerFollower").show();
}
}
function hideAnswer() {
// collect and store given answers
const scenario = scenarios[params.scenario];
const answers = {
player: player.gid,
scenario: params.scenario,
version: params.version,
infoDistribution: params.infoDistribution,
complete: answersLockedIn,
};
for (let i = 1; i <= scenario.dimensions.length; ++i) {
const dimInputs = $(`#dim${i}A,#dim${i}B,#dim${i}C,#dim${i}D,#dim${i}E`);
dimInputs.each((i, o) => {
const input = $(o);
const inputID = input.attr("id");
answers[inputID] = input.val();
});
}
if (!answermode || (answermode && player.isLeader)) {
store.addLog(XML(answermode ? "finalanswer" : "initialestimate", answers));
answers.intermediateLog = true;
submit(XML(answermode ? "finalanswer" : "initialestimate", answers));
}
$("#answerLeader").hide();
$("#answerFollower").hide();
$("#answerLocked").hide();
}
/**
*
*/
function showChat() {
hideAnswer();
$("#chat").show();
}
function hideChat() {
$("#chat").hide();
}
/**
*
*/
function endGame() {
hideAnswer();
$("#endgame").show();
if (store.params.sequence) {
sequence.endRound();
} else {
store.clear();
experimentComplete();
}
}