Commit graph

6 commits

Author SHA1 Message Date
2951a2d547
feat(multiplayer): game.action WS message for PlayerActions
Wire up the game.action WebSocket message so multiplayer games can
dispatch PlayerActions (F4b of post-epic-deferrals).

- Export PlayerAction/ActionResult from @paratype/chess barrel
- Add performAction wrapper to GameSession
- Protocol: GameActionMessageSchema + ClientMessage union update
- Server: handleGameAction handler with turn gate + error mapping
- Client: sendAction helper in useMultiplayerGame + net/types update
- Reuse game.state broadcast (no new server→client message type)

Unit tests: 1709 (baseline 1699 + 10 new: 5 protocol, 3 game-session, 2 net)
Playwright: 91 (baseline 89 + 2 new F4b multiplayer scenarios)
2026-04-21 11:56:35 -06:00
f2dff7e530
feat(multiplayer): host color preference (white/black/random)
Feature 1 of post-epic-deferrals. Lets the room creator pick which
color they play: white, black, or random. Joiner always takes the
remaining color (no joiner-side preference in v1 per plan decision
1b).

Protocol:
  - RoomCreatePayloadSchema gains an optional preferredColor field
    with the three-value enum. Omitting the field defaults to
    'white' on the server — byte-identical to pre-F1 behaviour so
    legacy clients are untouched.
  - Client type mirror (packages/chess/src/net/types.ts) kept in
    sync; exports PreferredColor type alias.

Server:
  - RoomRegistry.createRoom gains a preferredColor parameter.
    'random' is resolved at room creation via Math.random() and
    never leaks beyond this function — the concrete color is stored
    on room.creatorColor + room.joinerColor so reconnects surface
    the same assignment.
  - Room interface adds creatorColor + joinerColor fields.
  - JoinResult success variant widens from 'color: "black"' to
    'color: Color' to accept either assignment.
  - broadcast.ts threads payload.preferredColor through to
    createRoom.

UI:
  - Lobby.tsx adds a 3-button radiogroup ('Play as: White / Black /
    Random') between LayoutPicker and the modifier-profile picker.
    Active button is filled (bg-neutral-900), inactive is ghost.
    Omits the field from the room.create payload when left at the
    default so old servers keep working.
  - data-testid='color-preference-{white,black,random}' for e2e.

Tests:
  - 7 new protocol.test.ts cases: each enum value accepted,
    omitted valid, invalid strings rejected, composition with
    existing fields.
  - 5 new rooms.test.ts cases: back-compat default, explicit
    white, explicit black, random distribution across 40 samples,
    random-resolved color stored idempotently.
  - 4 new multiplayer.spec.ts cases: host=black flow, host=random
    disjoint-color assertion, back-compat no-field flow, UI
    buttons render + toggle with correct aria-checked state.

Verification: 1663 unit tests (+12) + 87/87 Playwright (+4).
Typecheck + lint clean across chess + server.

Plan: .sisyphus/plans/post-epic-deferrals.md Feature 1 complete.
2026-04-21 10:51:45 -06:00
ae87772277
feat(chess): drop-target hover indicator + personalized turn banner
Board now renders three visual states during a drag: faint dot for
legal quiet-move targets, ring for legal capture targets, and a bright
emerald fill when the cursor is actually over a valid drop square so
the user sees exactly where the piece will land. Hovering an invalid
square shows a subtle red tint telling the player the drag will snap
back on release.

Turn indicator shows "Your turn" (with a pulsing emerald dot and
green card treatment) when it`s the local player`s turn in
multiplayer, and "Opponent`s turn" otherwise. Solo play falls back
to the neutral "White`s turn" / "Black`s turn" phrasing.
2026-04-17 14:41:01 -06:00
de059fe707
feat(chess): per-color preset scope, turn-limited duration, server-authoritative sync
Presets previously lived on a process-global singleton with only a
binary on/off toggle. Two bugs followed:

1. Multiplayer illegal-move errors — the client-side toggle didn`t
   reach the server, so optimistic moves legal under client rules got
   rejected by the server`s unmodified ChessEngine.
2. No way to apply a rule to just white or just black, or to time-box
   it for N turns.

Replaces the shared `PRESET_REGISTRY.active: Set<string>` with
instance-owned `ChessEngine.activePresets: ActivePresetSet`. Each
activation carries:

  - scope: `both` | `white` | `black`
  - turnsRemaining: positive int or null (permanent)

Engine reads `getForColor(color)` per piece, so scope=white never
contributes moves during black`s turn. `applyMove` calls
`tickAfterMove(moverColor)` which implements player-local counting:
white-only durations tick only when white moves.

Compatibility is the LOOSE rule — `incompatibleWith` blocks only when
the two activations have overlapping scopes. `scope=white` + `scope=black`
pair of otherwise-incompatible presets is allowed because the engine
never evaluates both for the same side.

Server changes: GameSession owns an ActivePresetSet. New protocol
messages:

  - client → server: `room.setPresets` with full activation list
  - server → client: `game.presets` broadcast on every set change
    (post-setPresets + post-move-with-expiry)

`game.state` snapshots now include `activations` so reconnects pick
up the current rule set without extra round-trips.

Client changes: PredictionManager applies `game.presets` to the base
engine`s ActivePresetSet and re-renders via onStateChange; cloneEngine
carries activations onto the predicted clone. New hook surface:

  - activations: readonly PresetActivation[]
  - setPresets(next): replace the active set

useMultiplayerGame dispatches setPresets through the socket
(server-authoritative); useChessEngine mutates in-place (local mode).

UI: RulesDrawer + RulesView render scope radios (Both/White/Black)
and a duration input per active preset. Empty duration means
permanent, positive integers last N player-local turns.

Tests:

  - 15 new ActivePresetSet unit tests (scope, tick, loose compat,
    atomicity, clone)
  - 4 new engine-presets integration tests (per-color, duration,
    white-only vs black-only)
  - Migrated older preset tests from `PRESET_REGISTRY.activate` to
    the instance API
  - New E2E regression test: enable knights-leap-twice scope=white in
    multiplayer; verify the double-leap is accepted by the server,
    verify black`s knight cannot use it
2026-04-17 14:23:37 -06:00
5fb96647eb
fix(chess): wire multiplayer live sync via GameClient + PredictionManager
Previously, GameView used a local ChessEngine regardless of whether the
user was in a multiplayer room. Moves were never sent to the server and
the opponent only saw updates on full page reload.

Introduce useMultiplayerGame, a React hook that wraps GameClient and
PredictionManager and exposes the same shape as useChessEngine. App
reads sessionStorage once at mount of /game and dispatches to either
MultiplayerGameView (server-backed) or GameView (local) accordingly.

Board now accepts myColor to gate drag by piece ownership in addition
to turn, so black pieces never become draggable on white`s board and
vice versa.

Rewrites the multiplayer E2E to actually validate live sync: each drag
on one page is asserted to appear on the opposite page before the next
move. The previous test drove both colors from one page because the
GameClient wiring was missing; that workaround is no longer needed.
2026-04-17 13:54:06 -06:00
103f2bd0a6
test(root): E2E multiplayer with reconnect; tag Phase 4 (P4.12) 2026-04-16 18:12:44 -06:00