feat(server): modifier profile protocol schemas + error codes

Adds wire schemas and error codes for the modifier-profile feature:

- ModifierProfileSchema mirrored in server/protocol.ts (server pins a different zod major, so the chess-side schema cannot be re-exported directly). A keyof parity check guards against drift.

- RoomCreatePayloadSchema gains an optional 'profile' field — additive, existing callers unaffected.

- modifier-profile.update (client->server) payload schema with roomCode, newProfile, and version (for optimistic-concurrency checks).

- modifier-profile.updated (server->client) broadcast payload interface, typed for future promotion to the discriminated union when the broadcast is wired through rooms.

- Four new error codes: MODIFIER_PROFILE_INVALID / NO_KING / INVULN_KING / DEADLOCK, exported as const literals alongside the enum.

- Client wire types (packages/chess/src/net/types.ts) mirror all of the above: ModifierProfileWire shape, optional 'profile' on Room{Create,Created,Joined} payloads, and the new modifier-profile.* client/server message envelopes.

- PROTOCOL.md documents the new request/response flow and extends the error-code table.

Tests: 20 new cases across ModifierProfileSchema, RoomCreate profile integration, ModifierProfileUpdatePayloadSchema, and error-code acceptance.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-18 22:43:58 -06:00
commit 4b7d943edc
No known key found for this signature in database
5 changed files with 603 additions and 2 deletions

View file

@ -94,6 +94,9 @@ Response (Server → Client, type `room.created`):
- `layout`: resolved starting layout echoed back. Present on all
`room.created` and `room.joined` responses from new servers; may be
absent on legacy (pre-layouts) servers.
- `profile`: resolved modifier profile echoed back. Same shape as the
request-side `profile` field. Omitted when the room was created
without a profile, or when the server is pre-modifiers.
Error cases:
@ -101,6 +104,14 @@ Error cases:
- Layout fails validation (bad king count, duplicate squares,
unknown premade id, malformed FEN): `error` code `LAYOUT_INVALID`.
Message field carries the human-readable reason.
- Modifier profile fails validation: `error` code
`MODIFIER_PROFILE_INVALID` (schema / descriptor value-schema
failure) or one of the three carved-out invariants:
`MODIFIER_PROFILE_NO_KING`, `MODIFIER_PROFILE_INVULN_KING`,
`MODIFIER_PROFILE_DEADLOCK`. The `message` field carries a
human-readable reason; clients may use the code for targeted UI
hints (e.g. "Your profile leaves white with no king — please
adjust the setup").
### Message: room.join
@ -248,6 +259,95 @@ Payload:
}
```
### Message: modifier-profile.update
Direction: Client → Server
Purpose: Replace the room's active modifier profile. Applied by the
server at the NEXT TURN BOUNDARY — never mid-move.
Request payload:
```json
{
"type": "modifier-profile.update",
"roomCode": "ABC123",
"newProfile": {
"id": "buff-rooks",
"name": "Rook Reach",
"description": "All rooks get +1 range.",
"layoutId": "classic",
"perType": [
{ "kind": "range-bonus", "pieceType": "rook", "color": "both", "value": 1 }
],
"perInstance": [],
"version": 1,
"source": "custom"
},
"version": 3
}
```
- `roomCode`: the code of the room whose profile is being swapped.
- `newProfile`: the full replacement `ModifierProfile` (same shape as
the `profile` field on `room.create`). Partial updates are not
supported — the server treats the incoming profile as authoritative.
- `version`: the PROFILE version the client last observed (starts at
`0` before any update and is incremented by the server on each
successful apply). Submitting a stale version triggers an `error`
with code `MODIFIER_PROFILE_INVALID` so concurrent edits from both
players never silently overwrite.
On successful apply, the server broadcasts `modifier-profile.updated`
to both players (see below), followed by the usual `game.state` /
`game.delta` reflecting any piece-attribute changes.
Error cases:
- Profile fails schema / value-schema validation:
`MODIFIER_PROFILE_INVALID`
- Profile would leave a side with no king: `MODIFIER_PROFILE_NO_KING`
- Profile would make a king invulnerable (damage-resistance ≥ 1.0):
`MODIFIER_PROFILE_INVULN_KING`
- Profile's combined effects make the current position a legal-move
deadlock (neither side has any move): `MODIFIER_PROFILE_DEADLOCK`
- Room not found / client not a member: `ROOM_NOT_FOUND`
### Message: modifier-profile.updated
Direction: Server → Client
Purpose: Broadcast to both players after a successful
`modifier-profile.update`. Clients apply the new profile to their
local engine at the next turn boundary.
Payload:
```json
{
"type": "modifier-profile.updated",
"profile": {
"id": "buff-rooks",
"name": "Rook Reach",
"description": "All rooks get +1 range.",
"layoutId": "classic",
"perType": [
{ "kind": "range-bonus", "pieceType": "rook", "color": "both", "value": 1 }
],
"perInstance": [],
"version": 1,
"source": "custom"
},
"version": 4,
"appliedAt": "turn-boundary"
}
```
- `profile`: the newly-active modifier profile (same shape as
`room.create.profile`).
- `version`: the monotonically-incremented profile version. Clients
echo this on subsequent `modifier-profile.update` requests.
- `appliedAt`: always `"turn-boundary"` in v1. Reserved for future
policies (e.g. `"immediate"` for settings that apply mid-move).
### Message: error
Direction: Server → Client
@ -321,3 +421,8 @@ Messages counted: all messages from client including heartbeats.
| `MSG_TOO_LARGE` | Yes | Message exceeds 64KB |
| `BAD_TOKEN` | Yes | Token missing or invalid for room |
| `INVALID_MESSAGE` | Yes | JSON parse failure or schema validation failure |
| `LAYOUT_INVALID` | No | Starting layout failed validation |
| `MODIFIER_PROFILE_INVALID` | No | Modifier profile failed schema / descriptor validation, or stale `version` on an update |
| `MODIFIER_PROFILE_NO_KING` | No | Profile would leave a side without a king |
| `MODIFIER_PROFILE_INVULN_KING` | No | Profile would make a king invulnerable |
| `MODIFIER_PROFILE_DEADLOCK` | No | Profile would make the current position an unplayable deadlock |