feat(lobby): give MatchData a status instead of inferring it from seat occupancy - #1332
Open
tbsvttr wants to merge 3 commits into
Open
feat(lobby): give MatchData a status instead of inferring it from seat occupancy#1332tbsvttr wants to merge 3 commits into
tbsvttr wants to merge 3 commits into
Conversation
Server.MatchData had no field for "has this match started", so the lobby worked it out from occupancy: a match is running once every seat has a name. That only holds while the number of seats is fixed, and it is never true of a table you can join, which is what blocks boardgameio#1327. Matches now carry `status: 'open' | 'running'` and `creator`, the first player to sit down and the only one who may start it. The seat-occupancy rule is kept where it still applies, so nothing changes for fixed-seat matches: * joining the last free seat sets `running` by itself * freeing a seat sets `open` again, and hands `creator` to a player who is still seated if the one leaving held it POST /games/:name/:id/start settles the seats for matches that can begin before every seat is filled. It is the creator's to call — 403 for anyone else or for bad credentials, 409 if the match is already running — and LobbyClient gets a `startMatch` to match. `status` is optional in storage rather than required, because matches written before this field existed do not have one. Everything reads it through getMatchStatus, which falls back to the old occupancy rule for those — always the right answer, since every match predating the field has fixed seats. What the API hands back is not optional: createClientMatchData resolves it, so LobbyAPI.Match always carries a status.
The lobby decided whether to offer Play by looking for an unoccupied seat, which is the same inference the server no longer needs to make. It reads `status` instead. For a match with a fixed number of seats the two agree, so the buttons and the OPEN/RUNNING column are unchanged; for one that can start with seats to spare they no longer disagree.
The plugin exposed `player.opponent` when `ctx.numPlayers === 2`. That reads the number of seats the match was created with, which stops describing the match as soon as players can come and go, and leaves `opponent` pointing at a fixed '0' or '1' that may be nobody. What the game declares does hold for the life of the match, so `opponent` now follows `minPlayers` and `maxPlayers` both being 2. BREAKING: a two-player game that declares neither bound no longer gets `opponent`, and `player.opponent.set(...)` throws "Cannot read properties of undefined". Declaring `minPlayers: 2` and `maxPlayers: 2` restores it. Not addressed here: G.players is still built from ctx.numPlayers at setup, so a player seated after setup has no entry. That needs a hook and a way to be seated in the first place, neither of which exists yet — see boardgameio#1327.
tbsvttr
force-pushed
the
tbsvttr/lobby-match-status
branch
from
August 19, 2026 13:04
11a8275 to
15542d5
Compare
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.
Closes #1328.
Server.MatchDatanow carriesstatus: 'open' | 'running'andcreator, plusPOST /games/{name}/{id}/startandLobbyClient.startMatch. The seat-occupancy ruleis kept wherever it still applies, so a match with a fixed number of seats behaves
exactly as before.
The lifecycle
statusopenrunning, on its ownopen, andcreatorpasses to a player still seated/startrunningcreatoris the first player to sit down, and starting the match is theirs alone —403for anyone else or for bad credentials,409if it is already running.Two decisions worth flagging
statusis optional in storage, not required as the issue sketched it. Matcheswritten before this field existed do not have one, so a required field would be a
claim about stored data that isn't true. Everything reads it through
getMatchStatus,which falls back to the old occupancy rule for those rows — always the right answer,
since every match predating the field has fixed seats. What goes over the wire is not
optional:
createClientMatchDataresolves it, soLobbyAPI.Match['status']isrequired and the React lobby can rely on it.
Freeing a seat reopens the match. Otherwise a fixed-seat match that filled up and
then lost a player would sit at
runningwith an empty seat, where the lobby used toshow it as open again. This is the piece #1327 will want to revisit: once a match can
be started deliberately, a leave should probably not undo that.
plugin-player
opponentcame fromctx.numPlayers === 2, which reads the seat count the match wascreated with — it stops describing the match as soon as players can come and go, and
leaves
opponentpointing at a fixed'0'or'1'that may be nobody. It now followsminPlayersandmaxPlayersboth being 2, as the issue asks.Warning
This is a breaking change. A two-player game that declares neither bound no longer
gets
opponent, andplayer.opponent.set(...)throwsCannot read properties of undefined. DeclaringminPlayers: 2andmaxPlayers: 2restores it. The plugin'sown test game needed exactly that change, which is a fair sign real games will too —
say the word if you would rather it fell back to
ctx.numPlayerswhen a gamedeclares neither, and I will add that.
Not included
The plugin-player hook.
G.playersis still built fromctx.numPlayersat setup,so a player seated after setup gets no entry. Fixing that needs a hook and a way to
be seated in the first place, and neither exists yet — that is #1327's to do.
Creating at
minPlayers. The React create form already defaultsnumPlayerstominPlayers, and re-defaults it when the selected game changes, so there was nothingto change. It still lets you pick a larger number, which seemed worth keeping.
Testing
pnpm run lint,pnpm run ts,pnpm test(43 suites / 923 tests, 1 todo) andpnpm run buildall pass. Twenty new tests cover the start endpoint (creator, wrongplayer, bad credentials, missing playerID, already running, unknown match, and a match
stored before
statusexisted), the creator and auto-start on join, the reopen andcreator handover on leave, and
LobbyClient.startMatch.