Files
battleship/docs/openapi.yaml
T

369 lines
13 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/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/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/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:
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]
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' } }
Info:
type: object
required: [ok, phase, gameId, version, player1Available, player2Available, 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 }
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, 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] }
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' }