feat(plugin-multiplayer-vote): add anonymous group-vote primitive - #40
feat(plugin-multiplayer-vote): add anonymous group-vote primitive#40Mandyx22 wants to merge 4 commits into
Conversation
Add `plugin-multiplayer-vote`: an anonymous group poll. Every participant votes for one of the same options; the trial pushes that vote and barriers until all `expected_players` have voted, then optionally reveals the aggregate tally and the plurality winner. Unlike plugin-multiplayer-choice, the emphasis is the aggregate rather than the individual pick, and the ballot is anonymous: the data and reveal carry per-option counts and the winner, never a participant -> vote mapping (the tally is labelled from the trial's own `choices`, so no untrusted per-voter string is exposed). Winner is plurality, with `is_tie`/`tied_options` when the top count is shared and `winner: null` when nobody voted. Built as a barrier-then-reveal trial on the multiplayer API; supports a wait `timeout` (proceeds partial, flags `timed_out`), `reveal: false`, custom `button_html`, and exposes the pure core (tally/plurality/countVoted) as statics. The barrier counts only in-range votes so it agrees with the tally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A self-contained demo under examples/ that composes adapter-multiplayer-local + plugin-multiplayer-sync + plugin-multiplayer-vote: name entry -> lobby -> anonymous ballot -> tally/winner reveal. Runs entirely from two browser tabs with no server, following the chat-room.html / draw-room.html pattern. Verified end-to-end (two participants, winner rendered identically, no console errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p, note - Replace the live head-count `expected_players: () => namedPresent(...)` with a fixed EXPECTED_PLAYERS constant so all clients agree on how many votes the barrier waits for, 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, winner Sci-Fi 2/2, no console errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ctions 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 tally (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>
|
Closing in favor of #37: side-by-side review showed vote shared choice's entire choose → push → barrier → reveal engine and added no unique parameters, so its functionality has been folded into |
Summary
Adds
plugin-multiplayer-vote— an anonymous group poll, the last of the Tier-1/2 multiplayer primitives (afterlive-scoreboard#36,choice#37,match#38,turn#39).Every participant votes for one of the same options; the trial pushes that vote and barriers until all
expected_playershave voted, then optionally reveals the aggregate tally and the plurality winner.How it differs from
plugin-multiplayer-choicetally/winner/tied_options), never a participant→vote mapping. The tally is labelled from the trial's own trustedchoices, so no untrusted per-voter string is ever exposed. Each client still records/sees only its own pick (vote/vote_index+ a "(you)" reveal highlight).winner: null,is_tie: true,tied_optionslists the leaders); zero votes →winner: null, not a tie.Design
Barrier-then-reveal trial on the multiplayer API (a leaner
choice, minuspayoff/player_label):vote-core.ts, jsPsych-free, exposed as statics):readVote,countVoted,tally,plurality.countVoted(g, dataKey, choices.length)) so it always agrees with the tally — a stale/out-of-range index (e.g. left under a reuseddata_key) neither lifts the barrier early nor is dropped fromn_votes.timeout(proceeds partial, flagstimed_out),reveal: false, and custombutton_html.Tests
vote-core.spec.ts+index.spec.ts— 34 tests (unit + MockApi barrier flow +startTimelinesmoke test), including regression tests for the in-range barrier count.Review
Went through three high-effort
/code-reviewpasses; 6 findings surfaced and fixed (barrier/tally consistency, anonymity doc precision, tie-break example robustness, tie-state styling), final pass clean.🤖 Generated with Claude Code