feat: add plugin-multiplayer-choice - #37
Conversation
Add a new package for a simultaneous decision: every participant picks one of the same options, the trial pushes that pick and waits (a barrier) until all expected_players have chosen, then optionally reveals everyone's choices. It is the engine under simultaneous-move paradigms — prisoner's dilemma, public-goods contributions, dictator/coordination games — packaging the choose → push → wait → reveal flow as one declarative trial. Includes a timeout that degrades to a partial group, an optional payoff(choices, me) hook (off by default, so the plugin stays a pure decision primitive), the button_html/player_label display hooks, the pure core (collectChoices/countChosen) exposed as statics, docs, an example, and 27 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… tabs) A self-contained demo under examples/ that composes adapter-multiplayer-local + plugin-multiplayer-sync + plugin-multiplayer-choice: name entry -> lobby -> simultaneous Cooperate/Defect -> attributed reveal + PD payoffs. Runs entirely from two browser tabs with no server, following the chat-room.html pattern. Exercises player_label (lobby names on the reveal) and the payoff hook (the PD matrix). Verified end-to-end (asymmetric round: defector scored 5, cooperator 0, no console errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # package-lock.json
…nup, note - Replace the live head-count `expected_players: () => namedPresent(...)` with a fixed EXPECTED_PLAYERS constant, so all clients agree on the barrier size instead of hanging when tabs reach it at different moments. - Add a `pagehide` handler so a closed tab removes its slot rather than lingering as a ghost across runs. - State the required player count on the first screen (survey-text preamble). Verified end-to-end (2 tabs, asymmetric PD round 5/0, no console errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…jections jsPsych#3694 rejects a barrier timeout with a typed MultiplayerTimeoutError, but wait() can also reject if the condition predicate throws or the backend fails. Only a real timeout now degrades to a partial snapshot (timed_out, on_timeout); any other rejection rethrows so the trial halts loudly instead of being mislabelled a timeout. Mock rejects with the named error to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Pushed a robustness fix for the latest jsPsych#3694. The barrier's Changes (commit on this branch):
The existing push-failure test already covered the pre- |
Fold plugin-multiplayer-vote (PR #40) into plugin-multiplayer-choice — the two shared an identical choose → push → barrier → reveal engine and vote added no unique parameters, only a different reveal and data shape. - reveal_mode: "players" (default, attributed roster) | "tally" (anonymous per-option counts + plurality winner + tie summary, ported verbatim from vote). Invalid values throw rather than silently coerce. - record_choices_by_player: false drops the participant → choice map from the recorded data for output-anonymous polls. Docs state explicitly that this is output-level anonymity only — peers' raw picks remain readable in the shared session state by an inspecting client. - Always record the aggregate: tally / winner / is_tie / tied_options (choice-core gains tally() + plurality(), exposed as statics). - Adopt vote's option-range-bounded barrier count: a stale out-of-range pick under a reused data_key can no longer lift the barrier or skew n_players. - Disable the option CONTAINERS (not just inner <button>s) after a pick, so custom button_html without a <button> also greys out. - Add author to package.json; port vote's tally/tie/anonymity/stale-pick tests (44 tests total); add examples/poll-room.html (two-tab movie-night poll) and a tally example to the package example. - Regenerate package-lock.json from a clean npm install: the previous lock diff had stripped resolved/integrity fields from 808 entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Update (6d0d449): this PR now absorbs Review of the two plugins side-by-side showed vote shared an identical choose → push → barrier → reveal engine with choice and added zero unique parameters — its entire identity was the reveal rendering and data shape. Rather than ship two ~90%-duplicate packages, vote's functionality now lives here:
Also in this push:
#40 is being closed in favor of this PR. One naming note for the release: the CITATION.cff/package author is listed as "Mandy Liao" — @Mandyx22 please confirm that's the name you want on the published package. |
📦 New package — trusted-publishing bootstrap neededThis PR added one or more new packages. npm trusted publishing (OIDC) can't be configured for a package that doesn't exist yet, so a maintainer must do a one-time bootstrap per new package. After that, releases publish automatically via OIDC ( Before running the commands below: npm
|
Summary
Adds a new package,
@jspsych-multiplayer/plugin-multiplayer-choice— a simultaneous group-decision primitive. Every participant picks one of the same options; the trial pushes that pick and waits (a barrier) until allexpected_playershave chosen, then optionally reveals the outcome — either everyone's choices attributed (reveal_mode: "players", default) or an anonymous aggregate tally + plurality winner (reveal_mode: "tally"). Tally mode absorbs the separately-proposedplugin-multiplayer-vote(#40), which shared this plugin's entire engine and differed only in reveal/data shape.It is the engine under simultaneous-move paradigms — prisoner's dilemma, public-goods contributions, dictator/coordination games — packaging the choose → push → wait → reveal flow as one declarative trial. It sits alongside the other primitives (
sync,role,chat): likeplugin-multiplayer-syncit's a barrier (push → wait), but it owns the option UI and the "everyone has chosen" condition and adds a reveal.Lifecycle (3 phases)
choicesas buttons; the participant clicks one (recordschoice/choice_index/rt). The click listener is on the option container (like jsPsych'shtml-button-response), so a custombutton_htmlthat renders non-<button>markup is still selectable.pushreplaces the slot) andwaituntilcountChosen >= expected_players. Ontimeoutit proceeds with a partial group, flagstimed_out, and callson_timeout(a push failure, by contrast, surfaces loudly — not relabeled as a timeout).reveal: true) — list every player's choice (self highlighted, peer labels escaped), show the optional payoff, end on a continue button and/orreveal_duration.Design notes
collectChoices,countChosen,readChoice) inchoice-core.tswith its own spec, exposed as statics — the deterministic "who chose what" over a snapshot, testable without a live session.payoff(choices, me) => numberhook — off by default, so the plugin stays a pure decision primitive; with no hook you derive payoffs fromchoices_by_playerinon_finish.Parameters
choices(required),expected_players(required), plusprompt,button_html,data_key,waiting_message,timeout,on_timeout,reveal,reveal_mode,reveal_prompt,continue_label,reveal_duration,player_label,payoff,record_choices_by_player. Data:choice,choice_index,rt,wait_time,choices_by_player(nullwhen not recorded),n_players,tally,winner,is_tie,tied_options,my_payoff,timed_out,wait_error.Testing
reveal:false, timeout → partial, payoff (incl. throwing), push-failure propagation, slot-key preservation, HTML escaping,button_htmlwith non-<button>markup,reveal_durationauto-advance, and astartTimelinesmoke test through real jsPsych.tsc+ rollupbuildclean; repo-rootnpm run buildexit 0; repo-widenpm test299 passing / 13 suites; prettier clean.minor) included.🤖 Generated with Claude Code