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

@ -60,3 +60,16 @@ export {
type LayoutValidationResult,
} from "./layouts/index.js";
export { validateLayout } from "./layouts/validate.js";
// Piece modifier profiles — orthogonal to layouts and presets. Exposed
// so the server can type-check wire payloads against the authoritative
// chess-side shape. The zod schema itself is NOT re-exported here
// because the server package pins a different zod major; the server
// mirrors the schema locally (same shape) for wire validation.
export type {
ModifierProfile,
TypeModifier,
InstanceModifier,
ModifierKindId,
Direction,
} from "./modifiers/types.js";

View file

@ -28,7 +28,12 @@ export type ErrorCode =
| "MSG_TOO_LARGE"
| "BAD_TOKEN"
| "INVALID_MESSAGE"
| "LAYOUT_INVALID";
| "LAYOUT_INVALID"
// Modifier-profile rejections — mirrored from server/protocol.ts.
| "MODIFIER_PROFILE_INVALID"
| "MODIFIER_PROFILE_NO_KING"
| "MODIFIER_PROFILE_INVULN_KING"
| "MODIFIER_PROFILE_DEADLOCK";
// ---------------------------------------------------------------------------
// Starting layout wire shapes (mirrors server/src/protocol.ts)
@ -66,6 +71,50 @@ export interface Fact {
value: unknown;
}
// ---------------------------------------------------------------------------
// Modifier profile wire shapes (mirrors server/src/protocol.ts)
// ---------------------------------------------------------------------------
export type ModifierKindIdWire =
| "hp-bonus"
| "range-bonus"
| "direction-additions"
| "capture-flags"
| "promotion-override"
| "damage-resistance";
export interface TypeModifierWire {
kind: ModifierKindIdWire;
pieceType: PieceType;
color: Color | "both";
value: unknown;
}
export interface InstanceModifierWire {
kind: ModifierKindIdWire;
/** Algebraic notation, e.g. "b1". */
square: string;
value: unknown;
}
/**
* JSON-serializable shape of a ModifierProfile as it travels on the
* wire. Mirrors the chess-package `ModifierProfile` type structurally;
* kept as an independent interface here so the net-types layer doesn't
* need to import from the modifiers subsystem (and accidentally pull
* in the descriptor registry side-effects).
*/
export interface ModifierProfileWire {
id: string;
name: string;
description: string;
layoutId?: string;
perType: TypeModifierWire[];
perInstance: InstanceModifierWire[];
version: 1;
source: "premade" | "custom";
}
// ---------------------------------------------------------------------------
// Server → Client payloads
// ---------------------------------------------------------------------------
@ -124,6 +173,9 @@ export interface RoomCreatedPayload {
/** Resolved starting layout. Optional on the wire for backward
* compat with pre-layouts servers. */
layout?: ResolvedLayoutWire;
/** Modifier profile active at room creation. Optional for backward
* compat with pre-modifiers servers. */
profile?: ModifierProfileWire;
}
export interface RoomJoinedPayload {
@ -133,6 +185,21 @@ export interface RoomJoinedPayload {
activeRules: string[];
/** Resolved starting layout (see RoomCreatedPayload.layout). */
layout?: ResolvedLayoutWire;
/** Active modifier profile when this client joined (see
* RoomCreatedPayload.profile). */
profile?: ModifierProfileWire;
}
/**
* Server → client broadcast: the room's active modifier profile was
* replaced. Clients apply this to their local engine at the next turn
* boundary — the server guarantees the broadcast is ordered
* immediately before the ensuing `game.state` / `game.delta`.
*/
export interface ModifierProfileUpdatedPayload {
profile: ModifierProfileWire;
version: number;
appliedAt: "turn-boundary";
}
export interface ErrorPayload {
@ -150,6 +217,21 @@ export interface RoomCreatePayload {
/** Optional starting-layout selector. When omitted the server
* opens the room with the FIDE classic layout. */
layout?: LayoutRequest;
/** Optional inline modifier profile applied at room creation. */
profile?: ModifierProfileWire;
}
/**
* Client → server intent: swap the active modifier profile for the
* room. Server validates and applies at the next turn boundary, then
* broadcasts `modifier-profile.updated` to both players. `version` is
* the profile version the client last observed; stale requests are
* rejected with `MODIFIER_PROFILE_INVALID`.
*/
export interface ModifierProfileUpdatePayload {
roomCode: string;
newProfile: ModifierProfileWire;
version: number;
}
export interface RoomJoinPayload {
@ -182,6 +264,7 @@ export type ServerMessage =
| MessageEnvelope<"game.presets", GamePresetsPayload>
| MessageEnvelope<"room.created", RoomCreatedPayload>
| MessageEnvelope<"room.joined", RoomJoinedPayload>
| MessageEnvelope<"modifier-profile.updated", ModifierProfileUpdatedPayload>
| MessageEnvelope<"error", ErrorPayload>;
export type ClientMessage =
@ -189,4 +272,5 @@ export type ClientMessage =
| MessageEnvelope<"room.join", RoomJoinPayload>
| MessageEnvelope<"room.leave", Record<string, never>>
| MessageEnvelope<"game.move", GameMovePayload>
| MessageEnvelope<"room.setPresets", RoomSetPresetsPayload>;
| MessageEnvelope<"room.setPresets", RoomSetPresetsPayload>
| MessageEnvelope<"modifier-profile.update", ModifierProfileUpdatePayload>;