diff --git a/reference-game-cwg.html b/reference-game-cwg.html index 59730fb..432aef0 100644 --- a/reference-game-cwg.html +++ b/reference-game-cwg.html @@ -103,6 +103,24 @@ // every participant who does not click the download button contributes nothing. DATAPIPE_EXPERIMENT_ID: "", + // --- Lobby / no-match (#6) ------------------------------------------------------------------ + // B6 — how long a participant waits for a partner before being released and paid. NOT merely a + // UX number: it sets the advertised study duration (B5), which must include the expected wait, + // and it sets what the no-match payment below has to cover. Both are locked at publish, so + // this is locked at publish. ~5 min is a starting point, to be refined from observed arrival + // rate in the pilot (#12) BEFORE the study is published. + LOBBY_TIMEOUT_MS: 300000, + + // B2 — what an unmatched participant is paid. Full task rate for the wait, NOT Prolific's + // $0.14/min floor: a floor-rate payment loses to returning the study and taking a short + // survey, which teaches people to abandon the lobby at exactly the moment we need them to + // stay. Costs ~$10-15 across a whole run. + // + // Defined ONCE and templated into every screen that quotes it. Participant-facing copy that + // disagrees with what is actually configured is the failure this constant exists to prevent, + // and it is on the launch checklist for that reason. + NO_MATCH_PAYMENT_USD: 1.25, + // --- Dropout detection (#5) --------------------------------------------------------------- // Consecutive SILENT rounds before concluding the partner is gone. A round counts as silent // only if it timed out AND the partner sent no chat message during it — see the detector for @@ -565,6 +583,14 @@ let partnerDropped = false; let dropoutRound = null; + // No-match state (#6). Two different ways of never getting into a game — waiting out the lobby, + // and being the odd arrival when a pair has already formed. They share ONE completion code, + // because Prolific configures a single code per exit, and stay separable in the data via + // `no_match_reason` (A7). The two rates answer different questions: one is about arrival rate, + // the other about odd-numbered bursts, and #10's waiting room is sized off both. + let noMatch = false; + let noMatchReason = null; + // Trials the dyad actually completed, as opposed to rounds the timeline advanced through. Only // `ended_by: "submit"` counts — a timed-out round has a null assignment and is not a trial. // This is what makes a partial dyad usable rather than merely present (#8). @@ -591,6 +617,10 @@ n_trials_completed: completedTrials(), n_trials_scheduled: TRIALS, dropout_detected_at_round: dropoutRound, + // Both no-match routes share one completion code (A7), so this is the only thing that keeps + // "nobody arrived" separable from "I was the odd one out" in the data. They answer different + // questions and are sized differently in #10. + no_match_reason: noMatchReason, }); } @@ -602,13 +632,65 @@ }, }; + const money = (usd) => `$${usd.toFixed(2)}`; + + let lobbyTicker = null; + const lobbyTrial = { type: jsPsychMultiplayerSync, push_data: () => ({ name: myName, joinedAt: Date.now() }), - message: `
Waiting for ${MIN_PLAYERS} players to join…
-Open this page in another tab (keep the ?mp_session= in the URL) to add a player.
Waiting for a partner to join…
+This study is played in pairs, so we need to match you with someone before we can + start. Most people are matched quickly.
+Time remaining: —
+If we cannot find you a partner in time, we will + end the study and still pay you ${money( + CONFIG.NO_MATCH_PAYMENT_USD + )} for waiting. You do not need to do anything.
+Testing locally? Open this page in another tab,
+ keeping the ?mp_session= in the URL.
This game is already full. Thanks for your interest!
", + stimulus: () => { + const waited = + noMatchReason === "spectator_overflow" + ? `You arrived in time, but another pair had already formed, so there was no one + left for us to match you with. That is our scheduling problem, not anything you + did.
` + : `We could not find you a partner in the time available. This happens when not + enough people happen to be online at the same moment — it is nothing to do with + you or your responses.
`; + return `You will still be paid ${money( + CONFIG.NO_MATCH_PAYMENT_USD + )} for your time. Please submit below so we can process it.
+Saving…
+ ${submissionBlockHTML("no_match")}`; + }, choices: "NO_KEYS", - trial_duration: 4000, - on_finish: () => { - // Flush even here. A spectator has no game data, but their arrival and the fact they were - // turned away is the raw material for the odd-arrival rate #10's waiting room must handle. - recordOutcome("spectator"); - if (CONFIG.FLUSH_ON_ABORT) Pipeline.flush("spectator"); + on_load: () => { + // Button first, always. This screen exists so that someone who never got to play can + // still be paid; nothing may sit between them and that. + wireSubmissionButton("no_match"); + recordOutcome(noMatchReason ?? "no_match"); jsPsych.multiplayer.disconnect(); + // Worth saving even with no game data: arrival time and the reason are the raw material + // for the arrival-rate and odd-arrival numbers that size #10's waiting room and set the + // lobby timeout (B6) for the real run. + Pipeline.flush(`no-match-${noMatchReason ?? "unknown"}`).then((r) => { + const el = document.getElementById("save-status"); + if (!el) return; + el.textContent = r.skipped + ? "" + : r.ok + ? "Saved." + : r.timedOut + ? "Still uploading — you can submit now, this will finish in the background." + : "Upload failed. Please submit anyway and message the researcher."; + }); }, }, ], - conditional_function: () => myRole === "spectator", + conditional_function: () => noMatch, }; const gameRound = { @@ -819,11 +971,13 @@ preloadTrial, nameTrial, lobbyTrial, - roleTrial, - spectatorScreen, + pairingPhase, gameLoop, - // Exactly one of these two runs, and each carries its own completion code. gameLoop's own - // conditional cannot end it mid-game, so the abort comes from inside the round (#5). + // Exactly one of these three runs, and each carries its own completion code. Their + // conditions are mutually exclusive: noMatch excludes a role, a role excludes noMatch, and + // partnerDropped gates the complete screen. gameLoop's own conditional cannot end it + // mid-game, so the dropout abort comes from inside the round (#5). + noMatchScreen, partnerDroppedScreen, completeScreen, ]); diff --git a/tests/exits.test.mjs b/tests/exits.test.mjs new file mode 100644 index 0000000..049a455 --- /dev/null +++ b/tests/exits.test.mjs @@ -0,0 +1,165 @@ +// Tests for terminal-screen routing (#5, #6) in reference-game-cwg.html. +// +// node tests/exits.test.mjs +// +// Every way a session can end must land on EXACTLY ONE terminal screen, and each screen carries a +// different Prolific completion code. Two screens firing means a participant sees a contradiction; +// zero means they cannot submit and cannot be paid. Both have already happened in this file: +// a spectator used to fall through to "Game complete!" with the `complete` code, and a lobby +// timeout had no exit at all. +// +// The conditional_function bodies are extracted from the file and evaluated against each session +// state, so this tests the real routing rather than a description of it. + +import fs from "fs"; + +const html = fs.readFileSync(new URL("../reference-game-cwg.html", import.meta.url), "utf8"); +const scriptMatch = html.match(/