Skip to content

feat: add plugin-multiplayer-match - #38

Merged
jodeleeuw merged 9 commits into
mainfrom
feat/plugin-match
Jul 21, 2026
Merged

feat: add plugin-multiplayer-match#38
jodeleeuw merged 9 commits into
mainfrom
feat/plugin-match

Conversation

@Mandyx22

Copy link
Copy Markdown
Contributor

Summary

Adds a new package, @jspsych-multiplayer/plugin-multiplayer-match — partition a multiplayer group into matched sub-groups (pairs by default, or triads/larger) by deterministic consensus: every client independently computes the same partition from the shared group-session snapshot, with no coordinator and no extra round-trip.

It is the foundational primitive under pairwise / small-group paradigms — trust game, ultimatum, dyadic negotiation, partner coordination — and composes with the other primitives: pair up with match, assign a role within each pair via plugin-multiplayer-role (drive it off position), then play a round with plugin-multiplayer-choice.

Design

Mirrors plugin-multiplayer-role: a short barrier (push joinedAt/data → communicate wait → partition the resolved snapshot → publish store → save data), headless apart from a waiting message.

  • Pure core (match-core.ts, own spec): buildMatches(snapshot, opts) orders participants (from a stable id sort) and chunks them into groups of group_size. Reuses byId/hashSeed/mulberry32 from role's core so the partition is byte-identical on every client.
  • Strategies: ordered (by id), join_order (by joinedAt), random (seeded Fisher–Yates; seed defaults to a hash of the sorted ids + round, so pairings are unpredictable-by-id yet identical across clients, and re-pair each round).
  • Leftover policy for non-divisible counts: error (default — fail loud), spectator (leave extras unmatched), smaller_group (one undersized group). Unknown strategy/leftover values throw rather than silently falling back.
  • Fail-loud timeout (matched_self: false, timed_out: true); a config error (e.g. non-divisible with error) rejects the trial rather than being relabeled a timeout.
  • Store for downstream trials: getMyPartners / getMyGroup / getMyPosition / getMatchMap, plus buildMatches — all static members.

Parameters / data

group_size (default 2), expected_players, strategy, seed, round, leftover, ready, push_data, save_group, timeout, on_timeout, message. Data: match_group, partners, members, position, match_map, matched_self, timed_out, group.

Testing

  • New package: 29 tests (16 pure-core: pairing/triads/join_order/random-determinism/leftover-policies/enum-validation/edges; 13 plugin: barrier, partition, spectator, config-error-vs-timeout, timeout, join_order readiness, slot-key preservation, save_group, startTimeline smoke test).
  • tsc + rollup build clean; repo-root npm run build exit 0; repo-wide npm test 301 passing / 13 suites; prettier clean.
  • Changeset (minor) included.

🤖 Generated with Claude Code

Add a new package that partitions a multiplayer group into matched sub-groups
(pairs by default, or triads/larger) by deterministic consensus — every client
independently computes the same partition from the shared group-session snapshot,
with no coordinator. It is the foundational primitive under pairwise/small-group
paradigms (trust game, ultimatum, dyadic negotiation) and composes with
plugin-multiplayer-role (assign roles within a group via position).

Runs as a short barrier (like plugin-multiplayer-role): pushes joinedAt/data,
waits until the group is ready, partitions the resolved snapshot, publishes the
assignment to an accessor store, and saves it to the data record. Supports
ordered/join_order/random (seeded, per-round) pairing strategies and
error/spectator/smaller_group leftover policies for non-divisible counts (with
validation that throws on an unknown strategy/leftover), fails loud on timeout,
and exposes the pure core (buildMatches) plus partner accessors as statics.
Includes docs, an example, and 29 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mandyx22 and others added 5 commits July 13, 2026 16:52
A self-contained demo under examples/ composing adapter-multiplayer-local +
plugin-multiplayer-sync + plugin-multiplayer-match + plugin-multiplayer-choice:
name entry -> lobby -> match partitions the group into pairs by consensus ->
partner reveal -> each pair plays a Prisoner's Dilemma keyed per pair. Shows how
match (which has no UI of its own) composes into a real paired game, with
per-pair data_key/expected_players and a conditional_function for spectators.
Verified end-to-end (consistent pairing + seats, per-pair PD payoffs 0/5, no
console errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…screen

The demo passed a live head-count to `expected_players`
(() => namedPresent(...)), so tabs reaching the match barrier at different
moments could freeze on different counts and compute divergent pairings
("matched twice" / a leftover player who can't continue). expected_players must
be the same exact integer on every client for the consensus partition to agree.

- Replace the live count with a fixed EXPECTED_PLAYERS constant (default 4 → 2
  pairs, which actually shows the partitioning off), used for both the lobby
  threshold and match's expected_players.
- Add a preamble on the first screen stating the demo needs exactly N players,
  so anyone running it knows how many tabs to open.
- Update the README section accordingly.

Verified end-to-end with 4 tabs: two reciprocal pairs (Alice-Dave, Bob-Carol),
each plays its own PD, no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ut message

Addresses the review findings on match-room.html:
- Add a `pagehide` handler that disconnects the local adapter, so a CLOSED tab
  removes its slot instead of lingering as a "ghost" that later matches count as
  an absent player (the root of pairings that came out lopsided across runs).
- The spectator screen now reads the match trial's `timed_out` flag: a match
  timeout no longer masquerades as "there was an odd number of players".

Re-verified with 4 tabs: two reciprocal pairs, each plays its own PD, no errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
communicate() was removed from the jsPsych multiplayer API (jsPsych#3694).
Match now pushes then waits, and distinguishes a genuine readiness timeout
(MultiplayerTimeoutError, matched by error name) from other rejections: a
real timeout ends the trial gracefully, while a backend/push failure now
propagates loudly instead of being mislabelled as a timeout. Drops
communicate from the local API mirror + mock and adds a propagation test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@htsukamoto5

Copy link
Copy Markdown
Member

Pushed a fix for compatibility with the latest jsPsych#3694: communicate() was removed from the multiplayer API (commit 78f6c84), and this plugin still called api.communicate(payload, isReady, timeout), so the trial would break against a current core build.

Changes (commit on this branch):

  • Replaced the communicate() call with push().then(() => wait()).
  • The rejection handler now name-checks the error: only a genuine MultiplayerTimeoutError routes to the graceful timeout path (timed_out: true + on_timeout); any other rejection (e.g. a backend/push failure) rethrows so the trial halts loudly instead of masquerading as a timeout. Matching on error.name (not instanceof) survives two loaded copies of jspsych.
  • Dropped the now-removed communicate from the local MultiplayerApiLike mirror and the test mock, and set the mock's timeout rejection to name = "MultiplayerTimeoutError" so it mirrors the real API.
  • Added a regression test that a non-timeout rejection (push failure) propagates rather than being masked as a timeout.

tsc clean, 30/30 tests pass, build clean, patch changeset added. Same approach we used for plugin-multiplayer-role in #45.

Resolves the examples/README.md conflict by keeping all three sections
(choice-room, poll-room, match-room) and replaces the branch's degraded
package-lock.json — its previous diff stripped resolved/integrity fields
from 808 entries and carried a stray extraneous entry for the then-unmerged
choice package. The lockfile is now main's plus only the match workspace
entry, regenerated with a clean npm install.

Verified against the newly-merged choice plugin (tally-mode update):
match-room.html uses only unchanged plugin surface (data_key, player_label,
payoff, reveal, reveal_prompt, choices_by_player, my_payoff); full repo
build + 405 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jodeleeuw

Copy link
Copy Markdown
Member

Pushed 12a344d: merged main (which now includes plugin-multiplayer-choice, resolving this PR's ordering dependency for match-room.html) and regenerated package-lock.json from a clean npm install — the previous lock diff had stripped resolved/integrity hashes from 808 entries and carried a stray extraneous entry for the then-unmerged choice package; the diff vs. main is now just the 17-line match workspace entry. Verified match-room.html against the updated choice plugin (it only uses unchanged surface: data_key/player_label/payoff/reveal/choices_by_player/my_payoff); full repo build + 405 tests pass, CI green.

jodeleeuw and others added 2 commits July 21, 2026 08:18
- Merge main (countdown/public-goods/draw-room example sections woven into
  examples/README.md alongside match-room).
- Add author { name, url } to package.json — update-readme.js reads it to
  generate the root README contributor table.
- Remove CITATION.cff: the build then emits the default (empty) citations
  object instead of a hand-maintained one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jodeleeuw
jodeleeuw merged commit 3c9371e into main Jul 21, 2026
4 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 New package — trusted-publishing bootstrap needed

This 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 (publish.yml).

Before running the commands below: npm >=11.15.0 (npm install -g npm@latest) — required for npm trust; older versions fail the trust step with HTTP 400. Also 2FA enabled, logged in (npm login), with publish access to the @jspsych-multiplayer scope.

@jspsych-multiplayer/plugin-multiplayer-match

From a fresh checkout of main:

npm ci && npm run build
npm publish -w @jspsych-multiplayer/plugin-multiplayer-match --access public
npm trust github @jspsych-multiplayer/plugin-multiplayer-match --repo jspsych/jspsych-multiplayer --file publish.yml --allow-publish

Once bootstrapped, bump the version and merge to mainpublish.yml publishes future versions tokenlessly via OIDC, with provenance.

@github-actions github-actions Bot mentioned this pull request Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants