feat: add plugin-multiplayer-role (deterministic role assignment)#15
Open
htsukamoto5 wants to merge 10 commits into
Open
feat: add plugin-multiplayer-role (deterministic role assignment)#15htsukamoto5 wants to merge 10 commits into
htsukamoto5 wants to merge 10 commits into
Conversation
…tests Scaffold plugin-multiplayer-role and implement the API-independent layer: - roles.ts: deterministic assignRoles (join_order/random/rotate/rankBy/ roleFrom/custom), shared-seeded shuffle, loud overflow handling. - readiness.ts: makeReadiness gate (exact-count + strategy-derived field readiness; throwing speculative accessors treated as "not ready"). - store.ts: getMyRole/getRoleMap accessor + participantsByRole inverter. - roles.spec.ts + readiness.spec.ts: 23 tests, all passing. Plugin wrapper (index.ts) deferred until jsPsych PR #3694 merges, since it is the only piece that depends on the multiplayer API surface. Design doc in docs/role-assignment-plugin-plan.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + write README Replace the "balanced rotation not implemented" stub with a real Williams-design Latin-square rotation: balancedRotationShift(n, round) shifts the base order by the round'th term of the balanced sequence 0, n-1, 1, n-2, ... instead of by `round`. This keeps the per-round frequency guarantee for all n and additionally balances first-order carryover across the group (exact for even n). Pure in (n, round), so the consensus property is preserved. Add tests covering the Williams sequence, divergence from plain rotate, frequency balance, cross-client consensus, the carryover proof (all 12 ordered role-pairs occur once for n=4), and edge sizes. Replace the placeholder README with full docs: param/data tables, strategies (with strategy-vs-rank_by/role_from precedence), the rotate/balanced section, accessor usage, and the membership-consensus caveat. A Status note flags the trial wrapper as pending jsPsych#3694. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ll CITATION author
Add roles.property.spec.ts covering the consensus core's defining
invariants, checked exhaustively where cheap (every snapshot key
permutation for n <= 6) and with representative reorderings beyond:
- consensus invariance to snapshot key order, for every strategy
(incl. tie-break cases where joinedAt/score are all equal);
- every assignment is a bijection over the declared roles;
- rotate (plain and balanced) sweeps each role to each participant
exactly once per n consecutive rounds, for all n;
- balanced rotate balances first-order carryover for even n (all
n*(n-1) ordered role pairs occur once) — confirmed up to n=8.
Chose exhaustive/representative loops over a fast-check dependency: the
input space is small and structured (behaviour fixed by n and round mod
n), so exhaustive small-n is stronger evidence than random sampling and
adds no dependency surface to the monorepo.
Also fill in the CITATION.cff author/contact (Hannah Tsukamoto + ORCID);
release-time fields (version/doi/date/url) left as template TODOs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ics; honest deferred stub Make the package honest about what it ships without breaking the jsPsych single-default-export build convention: - Expose the pure core and role accessors as STATIC members of the plugin class (MultiplayerRole.assignRoles / .getMyRole / .getRoleMap / .participantsByRole). Reachable through the one default export, so the rollup `exports: "default"` convention (and CJS/IIFE ergonomics) stays intact — named runtime re-exports would have broken the build. Public types stay as type-only exports (erased at build, so they don't count). - Replace the scaffold stub: empty the placeholder param/data schema and make trial() throw a clear "wrapper not implemented, pending jsPsych #3694" error instead of silently finishing with junk data. Real JSDoc. - Replace the misleading "should load" test (which only exercised the stub) with tests of the real surface: statics present, accessors empty pre-assignment, assignRoles works through the public path, trial throws. - README: import default + use MultiplayerRole.<helper> static access. - Metadata: real package.json description/author; CITATION version 0.0.0 -> 0.0.1 to match package.json (other release fields stay TODO). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l MultiplayerAPI interface Replace the deferred throwing stub with the real trial() + handleTimeout() per plan §6, built against a local MultiplayerApiLike interface (new src/multiplayer-api.ts) so there is no build-time dependency on the unmerged jsPsych#3694. Restore the full info.parameters and info.data schema. Flow: guard participantId (and require `ready` for a custom-fn strategy) -> round-scoped push (first-seen joinedAt, rounds[round] merge) -> makeReadiness gate -> communicate(payload, isReady, timeout) (one .catch covers push and timeout) -> assignRoles over the resolved snapshot (no TOCTOU) -> setMyAssignment -> finishTrial; timeout/error -> handleTimeout fails loud and always ends the trial. Rewrite index.spec.ts with a mock API (in-memory session, reject-on-timeout via fake timers): 7 wrapper tests plus the kept static-surface tests. Update README status note and docs param/data tables + examples. 162 tests green, tsc clean, rollup build green (default-export convention intact). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tead of masking as timeout trial() now returns its promise and uses a two-arg .then so that a throw from assignRoles (overflow with no overflow_role, bad role_from, throwing custom fn) surfaces loudly as a config bug rather than being relabelled as a readiness timeout. Only communicate()'s own rejection routes to the soft timeout path. Also fire on_load() after rendering, since jsPsych does not auto-fire it for promise-returning trials. Add tests for the custom-strategy spectator case and config-error propagation; clarify assigned_self docs (overflow is in the map). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ustness Address pre-review findings (the High one is a real consensus hazard): - HIGH: replace locale-dependent `localeCompare` tie-breaks (join_order and rank_by) with a code-unit comparator `byId`, matching the code-unit base sort. `String.prototype.localeCompare` uses the runtime's locale/ICU collation, so two clients in different locales could order tied ids differently and compute divergent role maps — the exact property the plugin exists to guarantee. `byId` is now the single ordering rule (also used for readiness ctx.ids). - Warn when group_size and a custom ready are both omitted: readiness can otherwise resolve over a partial group the instant this client pushes. - handleTimeout wraps the on_timeout hook in try/catch so a throwing hook no longer skips finishTrial (which would reintroduce the hang the timeout prevents). - Friendly error when `roles` is missing/empty instead of a cryptic TypeError. - Docs: ctx.seed caveat, overflow_role applies whenever count exceeds slots, corrected rotate-formula sign, strengthened membership-consensus caveat. Fix repository.directory / homepage. - Tests: code-unit tie-break (mixed-case ids), roles validation, role_from precedence over rank_by/strategy, partial-group warning, throwing on_timeout still finishes. 170 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itation-js Match the official jsPsych plugin convention: - jspsych moves from a hard dependency to a peerDependency (>=8.0.0) plus a devDependency, so the published plugin uses the host's jsPsych instead of bundling its own (avoids a duplicate-instance ParameterType/instanceof mismatch). - @citation-js/* are removed from dependencies — citation generation is a build-time concern handled by @jspsych/config, not a runtime dep. Official plugins that use the citations feature list no @citation-js deps either. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…entative one The generated examples/index.html instantiated the plugin with no roles and no connected adapter, so it threw on load. Replace it with an illustrative example showing real usage — roles/strategy/group_size and getMyRole() for downstream timeline branching — with a clear note that it requires a connected multiplayer adapter and cannot run standalone until jsPsych#3694 lands (the runnable end-to-end demo is the forthcoming ultimatum-game example). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
@jspsych-multiplayer/plugin-multiplayer-role, a role-assignment plugin for the jsPsych multiplayer API. It deterministically maps participants in a shared group session to roles, so every client independently computes the same assignment without a coordinator — the consensus problem researchers otherwise hand-roll.This is a new community plugin I built on my own initiative, as a reusable primitive for multiplayer (especially turn-based economic-game) experiments.
What it does
["proposer", "responder"]or{ leader: 1, follower: 3 }— and pick a strategy:join_order, shared-seededrandom,rotate(per-round, with abalancedWilliams-sequence variant for frequency + first-order carryover balance)rank_by/role_from/ a fully custom functiongetMyRole()/getRoleMap()/participantsByRole().id -> { role, group? }reservesgroupfor future partitioning.Design notes
assignRoles()core with exhaustive property tests. The plugin wraps it plus the readiness gate.MultiplayerAPIand reaches the real object with one cast — the single seam to re-verify once #3694 lands. Tests run against an in-memory mock.overflow_role, arole_fromreturning an undeclared role, a throwing custom fn) propagate loudly rather than being mislabelled as a readiness timeout.output.exports: "default").Testing
164 tests green (pure core + property tests + wrapper),
tscclean, rollup build green.Independence
This plugin sidesteps the unreleased core via its local interface, so it does not depend on the sync/jatos migration PRs and can be reviewed/merged on its own.
🤖 Generated with Claude Code