openapi: 3.1.0
info:
  title: Arena Hero Game HTTP API
  version: 0.1.0
  summary: Submit complete command plans for the current Arena Hero Tick.
  description: |
    The HTTP half of the Arena Hero game loop. Authoritative tick, state, and
    plan receipts are delivered through the WebSocket contract in asyncapi.yaml.
  license:
    name: Documentation source
    identifier: LicenseRef-Arena-Hero
externalDocs:
  description: Human-readable game API documentation
  url: https://arena-hero.github.io/arena-hero-doc/api/overview
servers:
  - url: https://api.arenahero.io
    description: Production
tags:
  - name: Commands
    description: Replace the complete plan for one player, Tick, and source.
paths:
  /api/v1/game/commands:
    post:
      tags: [Commands]
      operationId: submitCommandPlan
      summary: Replace the current Agent plan
      description: |
        Persists one complete source plan during the current OPEN command
        window. A successful response confirms persistence, not dynamic action
        success. Every later successful POST replaces the entire earlier plan
        from that source.

        Idempotency hashes the exact raw request bytes. Reuse the same
        Idempotency-Key only to retry the byte-identical body; changing
        whitespace or object-key order counts as a different body. Completed
        responses are retained for seven days.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommandPlan'
            examples:
              rangerAndSpawn:
                summary: Ranger shot and Vanguard spawn
                value:
                  tick: 10583
                  unit_actions:
                    9d3e4941-2816-4a39-a220-df8cd95e877d:
                      type: SHOOT
                      target_id: 175f47f4-f7de-4785-b45c-9a2d2289a8ea
                      expected_cell: [120, 85]
                  core_action:
                    type: SPAWN
                    unit_type: VANGUARD
              emptyPlan:
                summary: No explicit Agent actions
                value:
                  tick: 10583
                  unit_actions: {}
      responses:
        '202':
          description: Complete source plan persisted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedReceipt'
        '400':
          description: Invalid JSON or idempotency key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidJson:
                  value:
                    error: INVALID_JSON
                    message: request body must contain one valid JSON object
                invalidKey:
                  value:
                    error: IDEMPOTENCY_KEY_INVALID
        '401':
          description: Missing or inactive bearer credential.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: UNAUTHORIZED
        '409':
          description: Tick, window, or idempotency conflict.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandErrorResponse'
              examples:
                closed:
                  value:
                    accepted: false
                    error: COMMAND_WINDOW_CLOSED
                tickMismatch:
                  value:
                    accepted: false
                    error: TICK_MISMATCH
                    tick: 10582
                    current_tick: 10583
                idempotencyConflict:
                  value:
                    accepted: false
                    error: IDEMPOTENCY_CONFLICT
        '413':
          description: Request body exceeds the configured command-body limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: REQUEST_BODY_TOO_LARGE
                message: request body is too large
        '415':
          description: Content-Type is not application/json.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: UNSUPPORTED_MEDIA_TYPE
                message: Content-Type must be application/json
        '422':
          description: The complete plan failed static validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
        '429':
          description: Concurrent-body or per-source Tick limit reached.
          headers:
            Retry-After:
              schema:
                type: integer
                minimum: 1
              description: Suggested delay in seconds.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitErrorResponse'
              examples:
                concurrency:
                  value:
                    error: COMMAND_CONCURRENCY_LIMIT
                rate:
                  value:
                    accepted: false
                    error: COMMAND_RATE_LIMITED
        '500':
          description: Internal server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: INTERNAL_ERROR
        '503':
          description: The Tick state is not ready for commands.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandErrorResponse'
              example:
                accepted: false
                error: TICK_NOT_READY
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Opaque Arena Hero Agent credential. Never place it in a URL.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        8–128 visible ASCII bytes (0x21–0x7e), so spaces, tabs, and newlines are
        invalid. Reuse only for a byte-identical retry of one logical request.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: '^[\x21-\x7e]+$'
      example: agent-10583-plan-01
  schemas:
    Position:
      type: array
      prefixItems:
        - type: integer
          format: int64
        - type: integer
          format: int64
      minItems: 2
      maxItems: 2
      description: |
        Exactly [x, y] using signed 64-bit integers. Increasing x moves right;
        increasing y moves down.
      examples:
        - [120, 85]
    Direction:
      type: string
      enum: [UP, DOWN, LEFT, RIGHT]
      description: UP=[0,-1], DOWN=[0,1], LEFT=[-1,0], RIGHT=[1,0].
    UnitType:
      type: string
      enum: [WORKER, VANGUARD, RANGER]
    UUID:
      type: string
      format: uuid
      pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
      description: Canonical lowercase hyphenated UUID.
    WaitAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: WAIT
    MoveAction:
      type: object
      additionalProperties: false
      required: [type, direction]
      properties:
        type:
          const: MOVE
        direction:
          $ref: '#/components/schemas/Direction'
    HarvestAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: HARVEST
    DepositAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: DEPOSIT
    SweepAction:
      type: object
      additionalProperties: false
      required: [type, direction]
      properties:
        type:
          const: SWEEP
        direction:
          $ref: '#/components/schemas/Direction'
    ShootAction:
      type: object
      additionalProperties: false
      required: [type, target_id, expected_cell]
      properties:
        type:
          const: SHOOT
        target_id:
          $ref: '#/components/schemas/UUID'
        expected_cell:
          $ref: '#/components/schemas/Position'
    PickupBeaconAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: PICKUP_BEACON
    DropBeaconAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: DROP_BEACON
    UnitAction:
      description: |
        Strict discriminator union. A field not listed for the selected type
        makes the complete plan invalid, even when its value is null or empty.
      oneOf:
        - $ref: '#/components/schemas/WaitAction'
        - $ref: '#/components/schemas/MoveAction'
        - $ref: '#/components/schemas/HarvestAction'
        - $ref: '#/components/schemas/DepositAction'
        - $ref: '#/components/schemas/SweepAction'
        - $ref: '#/components/schemas/ShootAction'
        - $ref: '#/components/schemas/PickupBeaconAction'
        - $ref: '#/components/schemas/DropBeaconAction'
    CoreWaitAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: WAIT
    SpawnAction:
      type: object
      additionalProperties: false
      required: [type, unit_type]
      properties:
        type:
          const: SPAWN
        unit_type:
          $ref: '#/components/schemas/UnitType'
    RepairShieldAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: REPAIR_SHIELD
    StartMoveAction:
      type: object
      additionalProperties: false
      required: [type, direction]
      properties:
        type:
          const: START_MOVE
        direction:
          $ref: '#/components/schemas/Direction'
    CancelMoveAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: CANCEL_MOVE
    CorePickupBeaconAction:
      $ref: '#/components/schemas/PickupBeaconAction'
    CoreDropBeaconAction:
      $ref: '#/components/schemas/DropBeaconAction'
    CoreAction:
      description: |
        Strict discriminator union. A field not listed for the selected type
        makes the complete plan invalid, even when its value is null or empty.
      oneOf:
        - $ref: '#/components/schemas/CoreWaitAction'
        - $ref: '#/components/schemas/SpawnAction'
        - $ref: '#/components/schemas/RepairShieldAction'
        - $ref: '#/components/schemas/StartMoveAction'
        - $ref: '#/components/schemas/CancelMoveAction'
        - $ref: '#/components/schemas/CorePickupBeaconAction'
        - $ref: '#/components/schemas/CoreDropBeaconAction'
    CommandPlan:
      type: object
      additionalProperties: false
      required: [tick]
      description: |
        Complete replacement plan for one source and Tick. Omitted Unit and
        Core actions default to WAIT unless an explicit higher-precedence
        source supplies that action.
      properties:
        tick:
          type: integer
          format: int64
          minimum: 1
          description: Must equal the Tick from the state used to compute this plan.
        unit_actions:
          type: object
          default: {}
          description: |
            Map from canonical lowercase owned Unit UUID to one strict
            UnitAction. Omit the field or use {} when no Unit acts.
          propertyNames:
            $ref: '#/components/schemas/UUID'
          additionalProperties:
            $ref: '#/components/schemas/UnitAction'
        core_action:
          $ref: '#/components/schemas/CoreAction'
    AcceptedReceipt:
      type: object
      additionalProperties: false
      required: [accepted, tick, source, received_at]
      properties:
        accepted:
          const: true
          description: Confirms durable persistence, not dynamic action success.
        tick:
          type: integer
          format: int64
          description: Tick whose source plan was replaced.
        source:
          type: string
          enum: [AGENT, MANUAL]
          description: Source slot that was replaced.
        received_at:
          type: string
          format: date-time
          description: UTC database persistence timestamp in RFC 3339 format.
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Stable machine-readable error code.
        message:
          type: string
          description: Optional safe human-readable explanation. Do not branch on it.
      additionalProperties: true
    CommandErrorResponse:
      type: object
      required: [error]
      properties:
        accepted:
          type: boolean
        error:
          type: string
          enum:
            - COMMAND_WINDOW_CLOSED
            - TICK_MISMATCH
            - TICK_NOT_READY
            - COMMAND_RATE_LIMITED
            - IDEMPOTENCY_CONFLICT
        tick:
          type: integer
          format: int64
        current_tick:
          type: integer
          format: int64
      additionalProperties: false
    RateLimitErrorResponse:
      type: object
      required: [error]
      properties:
        accepted:
          const: false
          description: Present for the per-source Tick rate limit; absent for body concurrency.
        error:
          type: string
          enum: [COMMAND_CONCURRENCY_LIMIT, COMMAND_RATE_LIMITED]
      additionalProperties: false
    ValidationIssue:
      type: object
      additionalProperties: false
      required: [reason]
      properties:
        unit_id:
          $ref: '#/components/schemas/UUID'
        reason:
          type: string
          enum:
            - TICK_MUST_BE_POSITIVE
            - UNIT_NOT_OWNED
            - UNKNOWN_ACTION_TYPE
            - UNKNOWN_CORE_ACTION_TYPE
            - UNEXPECTED_ACTION_FIELDS
            - INVALID_DIRECTION
            - INVALID_UNIT_TYPE
            - TARGET_ID_REQUIRED
            - EXPECTED_CELL_REQUIRED
            - VANGUARD_CANNOT_HARVEST
            - RANGER_CANNOT_HARVEST
            - VANGUARD_CANNOT_DEPOSIT
            - RANGER_CANNOT_DEPOSIT
            - WORKER_CANNOT_SWEEP
            - RANGER_CANNOT_SWEEP
            - WORKER_CANNOT_SHOOT
            - VANGUARD_CANNOT_SHOOT
    ValidationErrorResponse:
      type: object
      additionalProperties: false
      required: [accepted, error, details]
      properties:
        accepted:
          const: false
        error:
          const: INVALID_COMMAND
        details:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/ValidationIssue'
