feat: add plugin-multiplayer-match - #38
Conversation
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>
# Conflicts: # package-lock.json
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>
|
Pushed a fix for compatibility with the latest jsPsych#3694: Changes (commit on this branch):
tsc clean, 30/30 tests pass, build clean, patch changeset added. Same approach we used for |
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>
|
Pushed 12a344d: merged main (which now includes |
- 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>
# Conflicts: # examples/README.md
📦 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-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 viaplugin-multiplayer-role(drive it offposition), then play a round withplugin-multiplayer-choice.Design
Mirrors
plugin-multiplayer-role: a short barrier (pushjoinedAt/data →communicatewait → partition the resolved snapshot → publish store → save data), headless apart from a waiting message.match-core.ts, own spec):buildMatches(snapshot, opts)orders participants (from a stable id sort) and chunks them into groups ofgroup_size. ReusesbyId/hashSeed/mulberry32fromrole's core so the partition is byte-identical on every client.ordered(by id),join_order(byjoinedAt),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).error(default — fail loud),spectator(leave extras unmatched),smaller_group(one undersized group). Unknownstrategy/leftovervalues throw rather than silently falling back.matched_self: false, timed_out: true); a config error (e.g. non-divisible witherror) rejects the trial rather than being relabeled a timeout.getMyPartners/getMyGroup/getMyPosition/getMatchMap, plusbuildMatches— 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
save_group,startTimelinesmoke test).tsc+ rollupbuildclean; repo-rootnpm run buildexit 0; repo-widenpm test301 passing / 13 suites; prettier clean.minor) included.🤖 Generated with Claude Code