Files
battleship/docs/openapi.yaml
T

453 lines
17 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: Battleship ESP32 API
version: 1.0.0
description: |
Local HTTP API for the ESP32-C6 Battleship MVP. All responses are JSON,
UTF-8, and carry `Cache-Control: no-store`. The ESP32 is authoritative.
WebSocket synchronization at `/ws` is documented in API_CONTRACT.md;
it is not an HTTP request/response operation and is therefore outside
this OpenAPI document.
servers:
- url: http://{device-address}
variables:
device-address:
default: 192.168.1.50
description: DHCP address of the ESP32 on the local network.
paths:
/api/info:
get:
summary: Read public device and slot availability
responses:
'200':
description: Public game metadata.
content:
application/json:
schema:
$ref: '#/components/schemas/Info'
/api/health:
get:
summary: Read device diagnostics without secrets
responses:
'200':
description: Current resource and network diagnostics.
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
/api/network/status:
get:
summary: Read unauthenticated network configuration status without credentials
responses:
'200': { description: Network state and Russian status message. }
/api/network/scan:
get:
summary: Start or retrieve a bounded unauthenticated Wi-Fi scan
responses:
'200': { description: `scanning` or a bounded list of SSIDs; no passwords. }
'409': { $ref: '#/components/responses/Conflict' }
/api/network/validate:
post:
summary: Validate credentials and persist them only after association succeeds
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [ssid, password]
properties:
ssid: { type: string, minLength: 1, maxLength: 32 }
password: { type: string, maxLength: 63, writeOnly: true }
responses:
'200': { description: Validation started; poll network status for result. }
'400': { $ref: '#/components/responses/BadRequest' }
'409': { $ref: '#/components/responses/Conflict' }
/api/network/delete:
post:
summary: Delete the saved network profile and enter fallback mode
responses:
'200': { description: Profile deleted. }
'409': { $ref: '#/components/responses/Conflict' }
/api/session/join:
post:
summary: Create a session and request a role
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/JoinRequest'
responses:
'200':
description: Session created.
content:
application/json:
schema:
$ref: '#/components/schemas/SessionCreated'
'400': { $ref: '#/components/responses/BadRequest' }
'409': { $ref: '#/components/responses/Conflict' }
'413': { $ref: '#/components/responses/PayloadTooLarge' }
/api/session/resume:
post:
summary: Resume a role using its opaque token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TokenRequest'
responses:
'200':
description: Session resumed.
content:
application/json:
schema:
$ref: '#/components/schemas/SessionResumed'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'413': { $ref: '#/components/responses/PayloadTooLarge' }
/api/session/leave:
post:
summary: Release only the authenticated session
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/GameRequest' } } }
responses: &recoveryResponses
'200': { $ref: '#/components/responses/RecoveryAccepted' }
'400': { $ref: '#/components/responses/BadRequest' }
'409': { $ref: '#/components/responses/Conflict' }
'413': { $ref: '#/components/responses/PayloadTooLarge' }
/api/session/profile-reset:
post:
summary: Release the authenticated session for a browser-local profile reset
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/GameRequest' } } }
responses: *recoveryResponses
/api/game/config:
post:
summary: Set the game mode (Player 1, lobby only)
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ConfigRequest'
responses: &commandResponses
'200': { $ref: '#/components/responses/CommandAccepted' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'413': { $ref: '#/components/responses/PayloadTooLarge' }
'503': { $ref: '#/components/responses/ServerBusy' }
/api/game/start:
post:
summary: Start a configured game (Player 1)
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GameRequest'
responses: *commandResponses
/api/game/shot:
post:
summary: Fire at an opponent cell
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ShotRequest'
responses: *commandResponses
/api/game/rematch:
post:
summary: Confirm a rematch after a finished game
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GameRequest'
responses: *commandResponses
/api/game/abort:
post:
summary: Abort a human game with a disconnected opponent
description: Available only to Player 1 in the locked human-versus-human case.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GameRequest'
responses: *commandResponses
/api/game/reset:
post:
summary: Player-only full in-memory game reset
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/GameRequest' } } }
responses:
'200': { $ref: '#/components/responses/RecoveryAccepted' }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'413': { $ref: '#/components/responses/PayloadTooLarge' }
/api/state:
get:
summary: Get a complete role-safe state snapshot
parameters:
- $ref: '#/components/parameters/Version'
- $ref: '#/components/parameters/SessionToken'
responses:
'200':
description: State view for the token role; without a token, a spectator-safe view.
content:
application/json:
schema:
$ref: '#/components/schemas/State'
'401': { $ref: '#/components/responses/Unauthorized' }
'413': { $ref: '#/components/responses/PayloadTooLarge' }
/api/statistics:
get:
summary: Get match and reboot-scoped cumulative statistics
parameters:
- $ref: '#/components/parameters/SessionToken'
responses:
'200':
description: Statistics only; no board cells are returned.
content:
application/json:
schema:
$ref: '#/components/schemas/Statistics'
'401': { $ref: '#/components/responses/Unauthorized' }
'413': { $ref: '#/components/responses/PayloadTooLarge' }
components:
parameters:
Version:
name: version
in: query
required: false
schema: { type: integer, minimum: 0, maximum: 4294967295 }
description: Last applied state version; a complete snapshot is always safe to consume.
SessionToken:
name: X-Session-Token
in: header
required: false
schema: { $ref: '#/components/schemas/Token' }
description: Omit only when a spectator-safe public view is intended.
responses:
RecoveryAccepted:
description: Recovery completed or an idempotent retry observed the same completed recovery.
content:
application/json:
schema: { $ref: '#/components/schemas/RecoveryAccepted' }
CommandAccepted:
description: Command accepted by the authoritative application layer.
content:
application/json:
schema: { $ref: '#/components/schemas/CommandAccepted' }
BadRequest:
description: Malformed JSON or invalid input.
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Unauthorized:
description: Invalid or expired token.
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Forbidden:
description: The role may not perform this command.
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Conflict:
description: Stale game, wrong phase, unavailable slot, or invalid turn/cell state.
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
PayloadTooLarge:
description: Request body or target exceeded its route bound.
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
ServerBusy:
description: Bounded command queue or serializer is unavailable.
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
schemas:
Token:
type: string
pattern: '^[0-9a-f]{32}$'
description: Opaque, reboot-scoped session token.
Role:
type: string
enum: [player1, player2, spectator, player]
description: '`player` atomically assigns the first available player slot; responses always contain player1 or player2.'
Mode:
type: string
enum: [human, bot]
Phase:
type: string
enum: [lobby, preparing, in_progress, finished, rematch_wait]
GameId:
type: integer
minimum: 0
maximum: 4294967295
JoinRequest:
type: object
required: [name, requestedRole]
additionalProperties: false
properties:
name: { type: string, minLength: 1, maxLength: 80, description: 120 Unicode scalar values after server validation. }
requestedRole: { $ref: '#/components/schemas/Role' }
TokenRequest:
type: object
required: [token]
additionalProperties: false
properties: { token: { $ref: '#/components/schemas/Token' } }
GameRequest:
type: object
required: [token, gameId]
properties:
token: { $ref: '#/components/schemas/Token' }
gameId: { $ref: '#/components/schemas/GameId' }
ConfigRequest:
allOf:
- $ref: '#/components/schemas/GameRequest'
- type: object
required: [mode]
properties: { mode: { $ref: '#/components/schemas/Mode' } }
ShotRequest:
allOf:
- $ref: '#/components/schemas/GameRequest'
- type: object
required: [x, y]
properties:
x: { type: integer, minimum: 0, maximum: 9 }
y: { type: integer, minimum: 0, maximum: 9 }
CommandAccepted:
type: object
required: [ok, version, gameId]
properties:
ok: { type: boolean, enum: [true] }
version: { $ref: '#/components/schemas/GameId' }
gameId: { $ref: '#/components/schemas/GameId' }
SessionCreated:
allOf:
- $ref: '#/components/schemas/CommandAccepted'
- type: object
required: [token, role]
properties:
token: { $ref: '#/components/schemas/Token' }
role: { $ref: '#/components/schemas/Role' }
SessionResumed:
allOf:
- $ref: '#/components/schemas/CommandAccepted'
- type: object
required: [role]
properties: { role: { $ref: '#/components/schemas/Role' } }
RecoveryAccepted:
type: object
required: [ok, resetReason, generation]
properties:
ok: { type: boolean, enum: [true] }
resetReason: { type: string, enum: [session_left, profile_reset, game_reset] }
generation: { $ref: '#/components/schemas/GameId' }
Info:
type: object
required: [ok, phase, gameId, version, player1Available, player2Available, player1Name, player2Name, spectatorsAvailable]
properties:
ok: { type: boolean, enum: [true] }
phase: { $ref: '#/components/schemas/Phase' }
gameId: { $ref: '#/components/schemas/GameId' }
version: { $ref: '#/components/schemas/GameId' }
player1Available: { type: boolean }
player2Available: { type: boolean }
player1Name: { type: string, maxLength: 20 }
player2Name: { type: string, maxLength: 20 }
spectatorsAvailable: { type: integer, minimum: 0, maximum: 8 }
Health:
type: object
required: [ok, uptimeMs, wifiState, freeHeapBytes, minimumFreeHeapBytes, largestFreeBlockBytes, connectedClients, rejectedInput, resetReason]
properties:
ok: { type: boolean, enum: [true] }
uptimeMs: { $ref: '#/components/schemas/GameId' }
wifiState: { type: string, enum: [not_configured, connecting, connected] }
freeHeapBytes: { $ref: '#/components/schemas/GameId' }
minimumFreeHeapBytes: { $ref: '#/components/schemas/GameId' }
largestFreeBlockBytes: { $ref: '#/components/schemas/GameId' }
connectedClients: { type: integer, minimum: 0, maximum: 12 }
rejectedInput: { type: integer, minimum: 0, maximum: 65535 }
resetReason: { type: integer }
Board:
type: string
pattern: '^[01234]{100}$'
description: 0 unknown/water, 1 revealed ship, 2 miss, 3 hit, 4 sunk hit.
MatchStatistics:
type: array
minItems: 4
maxItems: 4
items: { type: integer, minimum: 0 }
description: '[shots, hits, misses, shipsSunk]'
CumulativeStatistics:
type: array
minItems: 7
maxItems: 7
items: { type: integer, minimum: 0 }
description: '[games, wins, losses, shipsSunk, shots, hits, misses]'
State:
type: object
required: [type, version, gameId, phase, mode, viewer, turn, players, boards, wins, winner, statistics]
properties:
type: { type: string, enum: [state] }
version: { $ref: '#/components/schemas/GameId' }
gameId: { $ref: '#/components/schemas/GameId' }
phase: { $ref: '#/components/schemas/Phase' }
mode: { $ref: '#/components/schemas/Mode' }
viewer: { $ref: '#/components/schemas/Role' }
turn: { type: string, enum: [player1, player2] }
players:
type: array
minItems: 2
maxItems: 2
items: { type: string, maxLength: 20 }
boards:
type: array
minItems: 2
maxItems: 2
items: { $ref: '#/components/schemas/Board' }
wins:
type: array
minItems: 2
maxItems: 2
items: { type: integer, minimum: 0 }
winner:
nullable: true
type: integer
enum: [0, 1]
statistics:
type: array
minItems: 2
maxItems: 2
items: { $ref: '#/components/schemas/MatchStatistics' }
Statistics:
type: object
required: [ok, viewer, gameId, match, cumulative]
properties:
ok: { type: boolean, enum: [true] }
viewer: { $ref: '#/components/schemas/Role' }
gameId: { $ref: '#/components/schemas/GameId' }
match:
type: array
minItems: 2
maxItems: 2
items: { $ref: '#/components/schemas/MatchStatistics' }
cumulative:
type: array
minItems: 2
maxItems: 2
items: { $ref: '#/components/schemas/CumulativeStatistics' }
Error:
type: object
required: [ok, code, message, version]
properties:
ok: { type: boolean, enum: [false] }
code:
type: string
enum: [MALFORMED_JSON, PAYLOAD_TOO_LARGE, INVALID_NAME, INVALID_ROLE, INVALID_MODE, INVALID_COORDINATE, UNAUTHORIZED, NO_PLAYER_SLOT, NO_SPECTATOR_SLOT, FORBIDDEN_ROLE, WRONG_PHASE, NOT_YOUR_TURN, CELL_ALREADY_SHOT, STALE_GAME, SERVER_BUSY]
message: { type: string, maxLength: 80, description: Russian user-facing text. }
version: { $ref: '#/components/schemas/GameId' }