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(); } }