# 2. Define player commands

The game needs three commands: `join`, `move`, and `resume`. Define that vocabulary before opening a socket so malformed input never reaches the rules.

Create `src/command-schemas.ts`:


```ts
import { z } from 'zod';

export const version = '1';
export const room = z.string().regex(/^[a-z0-9-]{1,32}$/);
export const commands = {
    join: z.object({ room }).strict(),
    resume: z.object({ room }).strict(),
    move: z.object({
        cell: z.number().int().min(0).max(8),
        revision: z.number().int().min(0).max(9),
    }).strict(),
};
```


The room schema keeps identifiers small and predictable. A move carries both the chosen cell and the board revision from Step 1. Zod validates untrusted network data and supplies the inferred TypeScript payload types.

Next add the outgoing snapshot and event schemas in `src/messages.ts`, then create the shared contract in `src/contract.ts`:


```ts
import { z } from 'zod';

import { commands, room } from './command-schemas.js';
export { version } from './command-schemas.js';
export const messages = {
    ...commands,
    state: z.object({
        board: z.array(z.enum(['X', 'O']).nullable()).length(9),
        players: z.array(z.string()).max(2), revision: z.number().int().min(0).max(9),
        result: z.enum(['X', 'O', 'draw']).nullable(), turn: z.enum(['X', 'O']), you: z.enum(['X', 'O']),
        online: z.array(z.string()).max(2), room,
    }).strict(),
    notice: z.object({ text: z.string() }).strict(),
};

type Payloads = { [Key in keyof typeof messages]: z.infer<typeof messages[Key]> };
export type Snapshot = Payloads['state'];
export type ClientEvents = Pick<Payloads, 'join' | 'move' | 'resume'>;
// Protocol failures are error envelopes, not an application payload schema.
export type ServerEvents = Pick<Payloads, 'state' | 'notice'> & { error: never; 'redweb:result': null };
```



```ts
import { defineSocketContract } from 'redweb/contract';
import { messages, version } from './messages.js';

export const match = defineSocketContract(version, messages);
```


The `/match` URL will select the game service. The message `type` will select exactly one handler. We never add a second `action` switch inside a generic handler.

The same contract describes both sides: server handlers receive validated payloads, and optional non-HTML clients get typed events and requests. Redweb's generated page client will use the same handler metadata later without handwritten browser transport code.

**Checkpoint:** add schema tests for valid and invalid room names, cells, revisions, snapshots, and notices. Run `npx tsc --noEmit` again. We now have rules and a protocol, but still no listener or UI.
