{
  "openapi": "3.1.0",
  "info": {
    "title": "游戏厅机器人接口",
    "version": "1.0.0",
    "description": "群里小游戏的通用协议：**群友的机器人实现这些接口，荷官（Sherry）来调用**。荷官是裁判和牌局状态的唯一来源，机器人只做决定、自己在群里说话。说明文档见 [游戏厅通用协议](https://docs.linkong.dev/protocol/)。\n\n- 所有游戏共用 4 个接口，用路径里的 `{game}` 区分游戏；每个游戏的请求体、动作、事件在 components 里各占一组 schema（名字以游戏名开头）。\n- 签名：请求头 `X-Tavern-Timestamp`（Unix 秒）和 `X-Tavern-Signature`（`sha256=` + 十六进制），签名原文为 `时间戳.方法.路径.原始请求体`（方法大写，路径不含域名和查询串），密钥是双方私下约定的一串随机字符，**不是飞书的 App Secret**。签名不对或时间戳相差超过 300 秒返回 401。\n- 顺序：荷官给每个机器人的请求严格按先后一个个发，`seq` 从 1 起连续递增（荷官重启后重新从 1 开始）。做决定只看 decision 请求本身带的局面。\n- 兼容：看不懂的字段忽略；不支持的游戏在邀请时回 `accept: false` 或 404；只有不兼容的改动才升路径版本（/v2）。\n\n**增加一个游戏**：① `GameKey` 的 enum 加上游戏名；② 新增 `<游戏>RoundStart`、`<游戏>DecisionRequest`、`<游戏>Decision`、各个 `<游戏>…Event` schema；③ 把它们加进 `RoundStart`、`DecisionRequest`、`Decision`、`Event` 的 oneOf（前两个同时加进 discriminator 的 mapping）；④ 在 `tags` 里加一个同名标签，并写一份 `docs/games/<游戏>.md`。路径不用改。"
  },
  "servers": [
    {
      "url": "https://{host}/{base}",
      "description": "你的机器人的基础地址（私下交给鹿饮）",
      "variables": {
        "host": {
          "default": "your-bot.example.com"
        },
        "base": {
          "default": "tavern"
        }
      }
    }
  ],
  "security": [
    {
      "TavernTimestamp": [],
      "TavernSignature": []
    }
  ],
  "tags": [
    {
      "name": "通用",
      "description": "所有游戏共用的接口和数据结构"
    },
    {
      "name": "liars_bar",
      "description": "骗子酒馆，规格见 docs/游戏厅-骗子酒馆规格.md"
    },
    {
      "name": "bullet_poker",
      "description": "子弹德州，规格见 docs/游戏厅-子弹德州规格.md"
    }
  ],
  "paths": {
    "/v1/games/{game}/invitations": {
      "post": {
        "tags": [
          "通用"
        ],
        "operationId": "invite",
        "summary": "邀请上桌（你自己决定来不来）",
        "description": "开桌后先让真人报名；真人报完名（开桌人点开始，或报名时间到）荷官才邀请每个接入的机器人，30 秒内回复（大模型冷启动也来得及；能快就快，大家在等开局）；凑满 4 个真人就不发邀请。来不来由你的机器人自己决定：可以看桌上有谁、还剩几个空位，也可以看心情。来就回 `accept: true`，不来回 `false`；404、超时、出错都当作不来。回了 true 也可能没坐上：别的机器人先占满了座位，或者有真人来了、最后上桌的机器人要让位。没坐上的话后面不会再收到这一局的请求。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Game"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Invitation"
              },
              "example": {
                "protocol": 1,
                "seq": 1,
                "game": "liars_bar",
                "match_id": "lb10212009abcd",
                "chat_id": "oc_xxx",
                "humans": [
                  "鹿饮"
                ],
                "bots": [],
                "seats_left": 3,
                "mode": "classic",
                "rules": "你在飞书群里玩《骗子酒馆》（仿 Steam《Liar's Bar》的出牌模式）。规则：牌堆 Q、K、A 各 6 张，Joker 2 张（万能，算作目标牌）。每小局随机定一张目标牌，每人 5 张。轮到你时二选一：扣着出 1–3 张并声称“都是目标牌”（可以撒谎），或者质疑上一家。质疑就翻开上一家刚出的牌：有一张既不是目标牌也不是 Joker，就是他撒谎，他开枪；全是真的，质疑的人开枪。每人一把 6 格左轮、1 发实弹，开过的枪不重置，下一枪中弹概率是 1/(6-已开枪数)。中弹出局，活到最后的赢。轮到你时如果只剩你手里有牌，只能质疑。你只知道自己的手牌；pile 是本小局大家一共声称出了几张目标牌。"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "接不接",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitationReply"
                },
                "example": {
                  "accept": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "不支持这个游戏（等同 accept: false）"
          }
        }
      }
    },
    "/v1/games/{game}/matches/{match_id}/rounds/{round}": {
      "put": {
        "tags": [
          "通用"
        ],
        "operationId": "startRound",
        "summary": "新一轮的私有信息",
        "description": "每轮开始（比如新发了牌）调用，带着你这一轮的私有信息。同一轮重复收到时以最后一次为准。立即回复。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Game"
          },
          {
            "$ref": "#/components/parameters/MatchId"
          },
          {
            "$ref": "#/components/parameters/Round"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RoundStart"
              },
              "example": {
                "protocol": 1,
                "seq": 12,
                "game": "liars_bar",
                "match_id": "lb10212009abcd",
                "chat_id": "oc_xxx",
                "turn_id": 30,
                "you": {
                  "seat": 2,
                  "name": "初雪"
                },
                "round": 3,
                "target": "K",
                "hand": [
                  {
                    "slot": 1,
                    "card": "Q"
                  },
                  {
                    "slot": 2,
                    "card": "K"
                  },
                  {
                    "slot": 3,
                    "card": "JOKER"
                  },
                  {
                    "slot": 4,
                    "card": "A"
                  },
                  {
                    "slot": 5,
                    "card": "K"
                  }
                ],
                "order": [
                  {
                    "seat": 0,
                    "name": "鹿饮"
                  },
                  {
                    "seat": 1,
                    "name": "奶豆"
                  },
                  {
                    "seat": 2,
                    "name": "初雪"
                  }
                ],
                "first": 0
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "收到（响应体 `{}`）",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {}
              }
            }
          },
          "204": {
            "description": "收到（无响应体）"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/games/{game}/matches/{match_id}/turns/{turn_id}/decision": {
      "post": {
        "tags": [
          "通用"
        ],
        "operationId": "decide",
        "summary": "轮到你：给出动作",
        "description": "请求体带着做决定需要的完整局面。和真人一样每手 60 秒，在请求体的 `deadline_ms` 之内回复（荷官留 2 秒余量，一般 58000）；超时、出错、动作不合法、turn_id 对不上，这一手由荷官代打，连续 3 次失败本局不再找你。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Game"
          },
          {
            "$ref": "#/components/parameters/MatchId"
          },
          {
            "$ref": "#/components/parameters/TurnId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DecisionRequest"
              },
              "example": {
                "protocol": 1,
                "seq": 20,
                "game": "liars_bar",
                "match_id": "lb10212009abcd",
                "chat_id": "oc_xxx",
                "turn_id": 37,
                "you": {
                  "seat": 2,
                  "name": "初雪"
                },
                "round": 3,
                "target": "K",
                "hand": [
                  {
                    "slot": 2,
                    "card": "K"
                  },
                  {
                    "slot": 3,
                    "card": "JOKER"
                  },
                  {
                    "slot": 4,
                    "card": "A"
                  }
                ],
                "can_challenge": true,
                "must_challenge": false,
                "last_play": {
                  "seat": 1,
                  "name": "奶豆",
                  "count": 2
                },
                "pile": 3,
                "players": [
                  {
                    "seat": 0,
                    "name": "鹿饮",
                    "alive": true,
                    "cards_left": 4,
                    "shots_fired": 1,
                    "is_bot": false
                  },
                  {
                    "seat": 1,
                    "name": "奶豆",
                    "alive": true,
                    "cards_left": 2,
                    "shots_fired": 0,
                    "is_bot": true
                  },
                  {
                    "seat": 2,
                    "name": "初雪",
                    "alive": true,
                    "cards_left": 3,
                    "shots_fired": 2,
                    "is_bot": true
                  }
                ],
                "log": [
                  "第 3 小局开始，目标牌 K",
                  "鹿饮出了 1 张（声称都是 K），还剩 4 张。",
                  "奶豆出了 2 张（声称都是 K），还剩 2 张。"
                ],
                "chat": [
                  {
                    "name": "鹿饮",
                    "text": "初雪你手里肯定没 K"
                  }
                ],
                "deadline_ms": 58000,
                "ask": "move",
                "prompt": "轮到你：扣着出 1 到 3 张牌，声称都是目标牌 K，或者质疑上家",
                "rules": "你在飞书群里玩《骗子酒馆》（仿 Steam《Liar's Bar》的出牌模式）。规则：牌堆 Q、K、A 各 6 张，Joker 2 张（万能，算作目标牌）。每小局随机定一张目标牌，每人 5 张。轮到你时二选一：扣着出 1–3 张并声称“都是目标牌”（可以撒谎），或者质疑上一家。质疑就翻开上一家刚出的牌：有一张既不是目标牌也不是 Joker，就是他撒谎，他开枪；全是真的，质疑的人开枪。每人一把 6 格左轮、1 发实弹，开过的枪不重置，下一枪中弹概率是 1/(6-已开枪数)。中弹出局，活到最后的赢。轮到你时如果只剩你手里有牌，只能质疑。你只知道自己的手牌；pile 是本小局大家一共声称出了几张目标牌。",
                "choices": [
                  {
                    "id": "play:2",
                    "say": "出 K（2 号位），声称都是 K"
                  },
                  {
                    "id": "play:3",
                    "say": "出 Joker（3 号位），声称都是 K"
                  },
                  {
                    "id": "play:4",
                    "say": "出 A（4 号位），声称都是 K"
                  },
                  {
                    "id": "play:2,3",
                    "say": "出 K、Joker（2、3 号位），声称都是 K"
                  },
                  {
                    "id": "play:2,4",
                    "say": "出 K、A（2、4 号位），声称都是 K"
                  },
                  {
                    "id": "play:3,4",
                    "say": "出 Joker、A（3、4 号位），声称都是 K"
                  },
                  {
                    "id": "play:2,3,4",
                    "say": "出 K、Joker、A（2、3、4 号位），声称都是 K"
                  },
                  {
                    "id": "challenge",
                    "say": "质疑 奶豆 刚出的 2 张"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "动作（按路径里的 game 选对应游戏的 Decision schema）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Decision"
                },
                "examples": {
                  "choice": {
                    "summary": "通用：从 choices 里挑一个（推荐）",
                    "value": {
                      "turn_id": 37,
                      "choice": "play:3,4",
                      "line": "两张 K，信不信由你"
                    }
                  },
                  "liars_bar_play": {
                    "summary": "骗子酒馆：出 3、4 号位",
                    "value": {
                      "turn_id": 37,
                      "action": "play",
                      "slots": [
                        3,
                        4
                      ]
                    }
                  },
                  "liars_bar_challenge": {
                    "summary": "骗子酒馆：质疑",
                    "value": {
                      "turn_id": 37,
                      "action": "challenge"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/games/{game}/matches/{match_id}/events": {
      "post": {
        "tags": [
          "通用"
        ],
        "operationId": "receiveEvent",
        "summary": "公开事件",
        "description": "牌桌上发生的公开的事，事后通知，立即回复。想说话就用你自己的机器人发到 chat_id 对应的群里。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Game"
          },
          {
            "$ref": "#/components/parameters/MatchId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              },
              "examples": {
                "game_over": {
                  "summary": "本局结束（通用）",
                  "value": {
                    "protocol": 1,
                    "seq": 21,
                    "game": "liars_bar",
                    "match_id": "lb10212009abcd",
                    "chat_id": "oc_xxx",
                    "turn_id": 39,
                    "you": {
                      "seat": 2,
                      "name": "初雪"
                    },
                    "event": "game_over",
                    "winner": {
                      "seat": 1,
                      "name": "奶豆"
                    }
                  }
                },
                "liars_bar_shot": {
                  "summary": "骗子酒馆：开枪",
                  "value": {
                    "protocol": 1,
                    "seq": 21,
                    "game": "liars_bar",
                    "match_id": "lb10212009abcd",
                    "chat_id": "oc_xxx",
                    "turn_id": 39,
                    "you": {
                      "seat": 2,
                      "name": "初雪"
                    },
                    "event": "shot",
                    "seat": 2,
                    "name": "初雪",
                    "result": "click",
                    "shots_fired": 3,
                    "nth": 3
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "收到（响应体 `{}`）",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {}
              }
            }
          },
          "204": {
            "description": "收到（无响应体）"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "TavernTimestamp": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Tavern-Timestamp",
        "description": "发出请求时的 Unix 时间（秒）。和你的时钟相差超过 300 秒就拒绝。"
      },
      "TavernSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Tavern-Signature",
        "description": "`sha256=` + HMAC_SHA256(密钥, `时间戳.方法.路径.原始请求体`) 的十六进制。对收到的原始字节验签后再解析 JSON，比较用恒定时间函数。"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "签名不对或时间戳过期，不处理"
      }
    },
    "parameters": {
      "Game": {
        "name": "game",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/GameKey"
        }
      },
      "MatchId": {
        "name": "match_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "这一局的编号"
      },
      "Round": {
        "name": "round",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "description": "第几轮"
      },
      "TurnId": {
        "name": "turn_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "description": "回合号，回复时原样带回"
      }
    },
    "schemas": {
      "GameKey": {
        "type": "string",
        "enum": [
          "liars_bar",
          "bullet_poker"
        ],
        "description": "游戏名。新游戏上线时往这里加。"
      },
      "Seat": {
        "type": "object",
        "properties": {
          "seat": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "seat",
          "name"
        ]
      },
      "Common": {
        "type": "object",
        "description": "除邀请外，每个请求体都带的公共字段。看不懂的其他字段一律忽略。",
        "properties": {
          "protocol": {
            "const": 1,
            "description": "协议版本"
          },
          "seq": {
            "type": "integer",
            "minimum": 1,
            "description": "发给你的第几个请求，严格递增，可用于去重、判断先后"
          },
          "game": {
            "$ref": "#/components/schemas/GameKey"
          },
          "match_id": {
            "type": "string",
            "description": "这一局的编号，和路径一致"
          },
          "chat_id": {
            "type": "string",
            "description": "在哪个飞书群，台词发到这里"
          },
          "turn_id": {
            "type": "integer",
            "minimum": 0,
            "description": "本局回合号，从 0 开始只增不减；同一回合的几个请求可能共用一个 turn_id"
          },
          "you": {
            "$ref": "#/components/schemas/Seat"
          }
        },
        "required": [
          "protocol",
          "seq",
          "game",
          "match_id",
          "chat_id",
          "turn_id",
          "you"
        ]
      },
      "Invitation": {
        "type": "object",
        "properties": {
          "protocol": {
            "const": 1
          },
          "seq": {
            "type": "integer",
            "minimum": 1
          },
          "game": {
            "$ref": "#/components/schemas/GameKey"
          },
          "match_id": {
            "type": "string"
          },
          "chat_id": {
            "type": "string"
          },
          "humans": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "已经上桌的真人"
          },
          "bots": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "已经上桌的机器人"
          },
          "seats_left": {
            "type": "integer",
            "minimum": 0,
            "description": "还剩几个空位（座位上限含机器人）"
          },
          "mode": {
            "type": "string",
            "description": "玩法（骗子酒馆：classic / devil / chaos）；没有模式的游戏不带"
          },
          "rules": {
            "type": "string",
            "description": "这一局的规则白话说明，看了再决定来不来"
          }
        },
        "required": [
          "protocol",
          "seq",
          "game",
          "match_id",
          "chat_id",
          "humans",
          "bots",
          "seats_left"
        ]
      },
      "InvitationReply": {
        "type": "object",
        "properties": {
          "accept": {
            "type": "boolean"
          }
        },
        "required": [
          "accept"
        ]
      },
      "DecisionBase": {
        "type": "object",
        "description": "所有游戏的动作都有这两个字段，其余字段由各游戏定义",
        "properties": {
          "turn_id": {
            "type": "integer",
            "description": "和路径里的 turn_id 一致"
          },
          "action": {
            "type": "string"
          }
        },
        "required": [
          "turn_id",
          "action"
        ]
      },
      "GameOverEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "本局结束（所有游戏通用）",
            "properties": {
              "event": {
                "const": "game_over"
              },
              "winner": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Seat"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "null 表示没有赢家（中途散场）"
              }
            },
            "required": [
              "event",
              "winner"
            ]
          }
        ]
      },
      "RoundStart": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/LiarsBarRoundStart"
          },
          {
            "$ref": "#/components/schemas/BulletPokerRoundStart"
          }
        ],
        "discriminator": {
          "propertyName": "game",
          "mapping": {
            "liars_bar": "#/components/schemas/LiarsBarRoundStart",
            "bullet_poker": "#/components/schemas/BulletPokerRoundStart"
          }
        }
      },
      "DecisionRequest": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/LiarsBarDecisionRequest"
          },
          {
            "$ref": "#/components/schemas/BulletPokerDecisionRequest"
          }
        ],
        "discriminator": {
          "propertyName": "game",
          "mapping": {
            "liars_bar": "#/components/schemas/LiarsBarDecisionRequest",
            "bullet_poker": "#/components/schemas/BulletPokerDecisionRequest"
          }
        }
      },
      "Decision": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/ChoiceDecision"
          },
          {
            "$ref": "#/components/schemas/LiarsBarDecision"
          },
          {
            "$ref": "#/components/schemas/BulletPokerDecision"
          }
        ],
        "description": "推荐回 ChoiceDecision（choices 里的 id）；也可以按路径里的 game 回对应游戏自己的动作"
      },
      "Event": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/GameOverEvent"
          },
          {
            "$ref": "#/components/schemas/LiarsBarPlayEvent"
          },
          {
            "$ref": "#/components/schemas/LiarsBarChallengeEvent"
          },
          {
            "$ref": "#/components/schemas/LiarsBarShotEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerHandEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerRaiseEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerCallEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerAllinEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerFoldEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerSwappedEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerStreetEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerShowdownEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerShotEvent"
          },
          {
            "$ref": "#/components/schemas/BulletPokerHandWonEvent"
          }
        ]
      },
      "LiarsBarCard": {
        "type": "string",
        "enum": [
          "Q",
          "K",
          "A",
          "JOKER"
        ]
      },
      "LiarsBarHandCard": {
        "type": "object",
        "properties": {
          "slot": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "card": {
            "$ref": "#/components/schemas/LiarsBarCard"
          }
        },
        "required": [
          "slot",
          "card"
        ]
      },
      "LiarsBarPlayer": {
        "type": "object",
        "properties": {
          "seat": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "alive": {
            "type": "boolean"
          },
          "cards_left": {
            "type": "integer",
            "minimum": 0
          },
          "shots_fired": {
            "type": "integer",
            "minimum": 0,
            "description": "这把枪已空响几次；下一枪中弹概率 1/(6-shots_fired)"
          },
          "is_bot": {
            "type": "boolean"
          }
        },
        "required": [
          "seat",
          "name",
          "alive",
          "cards_left",
          "shots_fired",
          "is_bot"
        ]
      },
      "LiarsBarRoundStart": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "properties": {
              "game": {
                "const": "liars_bar"
              },
              "round": {
                "type": "integer",
                "minimum": 1
              },
              "target": {
                "type": "string",
                "enum": [
                  "Q",
                  "K",
                  "A"
                ],
                "description": "本小局目标牌"
              },
              "hand": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/LiarsBarHandCard"
                },
                "description": "你的手牌（私有）"
              },
              "order": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Seat"
                },
                "description": "座次"
              },
              "first": {
                "type": "integer",
                "description": "这一小局先出牌的座位"
              }
            },
            "required": [
              "round",
              "target",
              "hand",
              "order",
              "first"
            ]
          }
        ]
      },
      "LiarsBarDecisionRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "$ref": "#/components/schemas/ChoiceFields"
          },
          {
            "type": "object",
            "properties": {
              "game": {
                "const": "liars_bar"
              },
              "round": {
                "type": "integer"
              },
              "target": {
                "type": "string",
                "enum": [
                  "Q",
                  "K",
                  "A"
                ]
              },
              "hand": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/LiarsBarHandCard"
                }
              },
              "can_challenge": {
                "type": "boolean",
                "description": "本小局第一手为 false"
              },
              "must_challenge": {
                "type": "boolean",
                "description": "只剩你手里有牌时为 true，只能质疑"
              },
              "last_play": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "seat": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      },
                      "count": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 3
                      }
                    },
                    "required": [
                      "seat",
                      "name",
                      "count"
                    ]
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "上一手：谁、出了几张；第一手为 null"
              },
              "pile": {
                "type": "integer",
                "minimum": 0,
                "description": "本小局大家一共声称出了几张目标牌"
              },
              "players": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/LiarsBarPlayer"
                }
              },
              "log": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "最近的公开播报"
              },
              "chat": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "text": {
                      "type": "string"
                    }
                  }
                },
                "description": "牌局期间群里最近 10 条不是指令的发言"
              },
              "deadline_ms": {
                "type": "integer",
                "description": "这一手还有多少毫秒回复（每手 60 秒，荷官留 2 秒余量，一般 58000）"
              }
            },
            "required": [
              "round",
              "target",
              "hand",
              "can_challenge",
              "must_challenge",
              "last_play",
              "pile",
              "players"
            ]
          }
        ]
      },
      "LiarsBarDecision": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/LiarsBarPlay"
          },
          {
            "$ref": "#/components/schemas/LiarsBarChallenge"
          }
        ],
        "discriminator": {
          "propertyName": "action",
          "mapping": {
            "play": "#/components/schemas/LiarsBarPlay",
            "challenge": "#/components/schemas/LiarsBarChallenge"
          }
        }
      },
      "LiarsBarPlay": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DecisionBase"
          },
          {
            "type": "object",
            "description": "扣着出牌，一律声称是目标牌；must_challenge 为 true 时不能用",
            "properties": {
              "action": {
                "const": "play"
              },
              "slots": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 5
                },
                "minItems": 1,
                "maxItems": 3,
                "uniqueItems": true,
                "description": "要出的牌的位置号，必须在手里"
              }
            },
            "required": [
              "slots"
            ]
          }
        ]
      },
      "LiarsBarChallenge": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DecisionBase"
          },
          {
            "type": "object",
            "description": "质疑上一家；can_challenge 为 true 时才能用",
            "properties": {
              "action": {
                "const": "challenge"
              }
            }
          }
        ]
      },
      "LiarsBarPlayEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "有人出了几张",
            "properties": {
              "event": {
                "const": "play"
              },
              "seat": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "count": {
                "type": "integer"
              },
              "cards_left": {
                "type": "integer"
              },
              "game": {
                "const": "liars_bar"
              }
            },
            "required": [
              "event",
              "seat",
              "name",
              "count",
              "cards_left"
            ]
          }
        ]
      },
      "LiarsBarChallengeEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "有人质疑，翻牌结果",
            "properties": {
              "event": {
                "const": "challenge"
              },
              "challenger": {
                "$ref": "#/components/schemas/Seat"
              },
              "accused": {
                "$ref": "#/components/schemas/Seat"
              },
              "revealed": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/LiarsBarCard"
                }
              },
              "lied": {
                "type": "boolean",
                "description": "上一家有没有撒谎"
              },
              "shooter": {
                "$ref": "#/components/schemas/Seat"
              },
              "game": {
                "const": "liars_bar"
              }
            },
            "required": [
              "event",
              "challenger",
              "accused",
              "revealed",
              "lied",
              "shooter"
            ]
          }
        ]
      },
      "LiarsBarShotEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "开枪",
            "properties": {
              "event": {
                "const": "shot"
              },
              "seat": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "result": {
                "type": "string",
                "enum": [
                  "click",
                  "bang",
                  "misfire"
                ],
                "description": "click 空枪、bang 中弹出局、misfire 哑火"
              },
              "shots_fired": {
                "type": "integer"
              },
              "nth": {
                "type": "integer",
                "minimum": 1,
                "description": "这把枪自上次装弹以来的第几枪"
              },
              "game": {
                "const": "liars_bar"
              }
            },
            "required": [
              "event",
              "seat",
              "name",
              "result",
              "shots_fired",
              "nth"
            ]
          }
        ]
      },
      "BulletPokerCard": {
        "type": "string",
        "pattern": "^[2-9TJQKA][shdc]$",
        "description": "点数 + 花色：2–9、T(10)、J、Q、K、A；s 黑桃、h 红心、d 方块、c 梅花，比如 \"As\"、\"Td\""
      },
      "BulletPokerOptions": {
        "type": "object",
        "description": "轮到你时能做什么（不是你的回合时全是 false）",
        "properties": {
          "raise": {
            "type": "boolean",
            "description": "加注：再押 1 颗（押到 round+1 颗）"
          },
          "fold": {
            "type": "boolean",
            "description": "弃：马上把押的子弹全装进枪开一枪，这一手不玩了；这局弃满 3 次后为 false"
          },
          "swap": {
            "type": "boolean",
            "description": "换牌：每手 1 次；有人全押后不能换"
          },
          "allin": {
            "type": "boolean",
            "description": "全押：翻出 3 张公共牌（round ≥ 2）后才能用，押满 8 颗"
          },
          "call": {
            "type": "boolean",
            "description": "跟全押：有人全押时才有"
          },
          "pick": {
            "type": "boolean",
            "description": "刚换牌：从 offer 的 3 张里挑一张"
          },
          "responding": {
            "type": "boolean",
            "description": "有人全押，你只能跟或弃"
          }
        },
        "required": [
          "raise",
          "fold",
          "swap",
          "allin",
          "call",
          "pick",
          "responding"
        ]
      },
      "BulletPokerPlayer": {
        "type": "object",
        "properties": {
          "seat": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "alive": {
            "type": "boolean"
          },
          "folded": {
            "type": "boolean",
            "description": "这一手弃了"
          },
          "allin": {
            "type": "boolean",
            "description": "这一手全押或跟了全押"
          },
          "bet": {
            "type": "integer",
            "minimum": 0,
            "maximum": 8,
            "description": "这一手押了几颗"
          },
          "acted": {
            "type": "boolean",
            "description": "这一轮表过态了"
          },
          "swapped": {
            "type": "boolean",
            "description": "这一手换过牌了"
          },
          "folds_left": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3,
            "description": "这局还能弃几次"
          },
          "is_bot": {
            "type": "boolean"
          }
        },
        "required": [
          "seat",
          "name",
          "alive",
          "folded",
          "allin",
          "bet",
          "acted",
          "swapped",
          "folds_left",
          "is_bot"
        ]
      },
      "BulletPokerRoundStart": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "新的一手开始（路径里的 {round} 是第几手）",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "round": {
                "type": "integer",
                "minimum": 1,
                "description": "和 hand_no 相同：子弹德州的“一轮”就是一手（和路径里的 {round} 一致）"
              },
              "hand_no": {
                "type": "integer",
                "minimum": 1,
                "description": "第几手"
              },
              "hole": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BulletPokerCard"
                },
                "minItems": 2,
                "maxItems": 2,
                "description": "你的两张底牌（私有）"
              },
              "button": {
                "type": "integer",
                "description": "这一手从哪个座位开始表态（每手轮转）"
              },
              "order": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "seat": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "alive": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "seat",
                    "name",
                    "alive"
                  ]
                },
                "description": "座次"
              }
            },
            "required": [
              "round",
              "hand_no",
              "hole",
              "button",
              "order"
            ]
          }
        ]
      },
      "BulletPokerDecisionRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "$ref": "#/components/schemas/ChoiceFields"
          },
          {
            "type": "object",
            "description": "轮到你表态",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "hand_no": {
                "type": "integer",
                "minimum": 1
              },
              "round": {
                "type": "integer",
                "minimum": 1,
                "maximum": 4,
                "description": "第几轮表态：1 翻牌前、2 翻出 3 张后、3 翻出第 4 张后（4 是摊牌，不会来问你）"
              },
              "board": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BulletPokerCard"
                },
                "maxItems": 5,
                "description": "已经亮出来的公共牌"
              },
              "hole": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BulletPokerCard"
                },
                "minItems": 2,
                "maxItems": 2,
                "description": "你的底牌"
              },
              "bet": {
                "type": "integer",
                "minimum": 1,
                "maximum": 8,
                "description": "你这一手押了几颗；现在弃就装这么多"
              },
              "folds_left": {
                "type": "integer",
                "minimum": 0,
                "maximum": 3
              },
              "swapped": {
                "type": "boolean"
              },
              "options": {
                "$ref": "#/components/schemas/BulletPokerOptions"
              },
              "offer": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "slot": {
                        "type": "integer",
                        "enum": [
                          1,
                          2
                        ],
                        "description": "换的是第几张底牌"
                      },
                      "cards": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/BulletPokerCard"
                        },
                        "minItems": 3,
                        "maxItems": 3
                      }
                    },
                    "required": [
                      "slot",
                      "cards"
                    ]
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "刚换牌时的 3 张候选（只有你看得到），否则为 null"
              },
              "allin_by": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Seat"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "这一手谁全押了"
              },
              "button": {
                "type": "integer"
              },
              "players": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BulletPokerPlayer"
                }
              },
              "log": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "最近的公开播报"
              },
              "chat": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "text": {
                      "type": "string"
                    }
                  }
                },
                "description": "牌局期间群里最近 10 条不是指令的发言"
              },
              "deadline_ms": {
                "type": "integer",
                "description": "这次表态还有多少毫秒（每次 60 秒，荷官留 2 秒余量，一般 58000）"
              }
            },
            "required": [
              "hand_no",
              "round",
              "board",
              "hole",
              "bet",
              "folds_left",
              "swapped",
              "options",
              "offer",
              "allin_by",
              "button",
              "players"
            ]
          }
        ]
      },
      "BulletPokerBet": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DecisionBase"
          },
          {
            "type": "object",
            "description": "raise 加注、fold 弃、allin 全押、call 跟全押；按 options 里为 true 的选",
            "properties": {
              "action": {
                "enum": [
                  "raise",
                  "fold",
                  "allin",
                  "call"
                ]
              }
            }
          }
        ]
      },
      "BulletPokerSwap": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DecisionBase"
          },
          {
            "type": "object",
            "description": "换牌：丢掉第 slot 张底牌，荷官接着发一个带 offer 的 decision 请求让你挑",
            "properties": {
              "action": {
                "const": "swap"
              },
              "slot": {
                "type": "integer",
                "enum": [
                  1,
                  2
                ]
              }
            },
            "required": [
              "slot"
            ]
          }
        ]
      },
      "BulletPokerPick": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DecisionBase"
          },
          {
            "type": "object",
            "description": "从 offer 的 3 张里挑第 choice 张换上，荷官接着再问你这一轮表态",
            "properties": {
              "action": {
                "const": "pick"
              },
              "choice": {
                "type": "integer",
                "minimum": 1,
                "maximum": 3
              }
            },
            "required": [
              "choice"
            ]
          }
        ]
      },
      "BulletPokerDecision": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/BulletPokerBet"
          },
          {
            "$ref": "#/components/schemas/BulletPokerSwap"
          },
          {
            "$ref": "#/components/schemas/BulletPokerPick"
          }
        ]
      },
      "BulletPokerHandEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "新的一手开始（公开部分）",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "hand"
              },
              "hand_no": {
                "type": "integer"
              },
              "button": {
                "type": "integer"
              }
            },
            "required": [
              "event",
              "hand_no",
              "button"
            ]
          }
        ]
      },
      "BulletPokerRaiseEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "有人加注",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "raise"
              },
              "seat": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "bet": {
                "type": "integer"
              }
            },
            "required": [
              "event",
              "seat",
              "name",
              "bet"
            ]
          }
        ]
      },
      "BulletPokerCallEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "有人跟全押",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "call"
              },
              "seat": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "event",
              "seat",
              "name"
            ]
          }
        ]
      },
      "BulletPokerAllinEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "有人全押",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "allin"
              },
              "seat": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "event",
              "seat",
              "name"
            ]
          }
        ]
      },
      "BulletPokerFoldEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "有人弃了，接着就是他的开枪事件",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "fold"
              },
              "seat": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "load": {
                "type": "integer",
                "description": "装了几颗"
              }
            },
            "required": [
              "event",
              "seat",
              "name",
              "load"
            ]
          }
        ]
      },
      "BulletPokerSwappedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "有人换了一张底牌（不公布换成什么）",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "swapped"
              },
              "seat": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "event",
              "seat",
              "name"
            ]
          }
        ]
      },
      "BulletPokerStreetEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "翻公共牌",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "street"
              },
              "round": {
                "type": "integer"
              },
              "board": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BulletPokerCard"
                }
              }
            },
            "required": [
              "event",
              "round",
              "board"
            ]
          }
        ]
      },
      "BulletPokerShowdownEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "摊牌：亮出没弃的人的底牌，接着是输家的开枪事件",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "showdown"
              },
              "board": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BulletPokerCard"
                }
              },
              "winners": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Seat"
                }
              },
              "hands": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "seat": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "hole": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BulletPokerCard"
                      }
                    },
                    "hand": {
                      "type": "string",
                      "description": "牌型中文名，比如“葫芦”"
                    }
                  },
                  "required": [
                    "seat",
                    "name",
                    "hole",
                    "hand"
                  ]
                }
              }
            },
            "required": [
              "event",
              "board",
              "winners",
              "hands"
            ]
          }
        ]
      },
      "BulletPokerShotEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "开枪",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "shot"
              },
              "seat": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "load": {
                "type": "integer",
                "minimum": 1,
                "maximum": 8,
                "description": "装了几颗（8 格左轮，中弹概率 load/8）"
              },
              "result": {
                "type": "string",
                "enum": [
                  "click",
                  "bang",
                  "misfire"
                ],
                "description": "click 空枪、bang 中弹出局、misfire 哑火"
              }
            },
            "required": [
              "event",
              "seat",
              "name",
              "load",
              "result"
            ]
          }
        ]
      },
      "BulletPokerHandWonEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Common"
          },
          {
            "type": "object",
            "description": "没摊牌就分出了这一手：别人都弃了（folds），或者全押没人跟（allin，不亮牌）",
            "properties": {
              "game": {
                "const": "bullet_poker"
              },
              "event": {
                "const": "hand_won"
              },
              "winners": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Seat"
                }
              },
              "reason": {
                "type": "string",
                "enum": [
                  "folds",
                  "allin"
                ]
              }
            },
            "required": [
              "event",
              "winners",
              "reason"
            ]
          }
        ]
      },
      "Choice": {
        "type": "object",
        "description": "一个合法选项：id 原样回给荷官，say 是给人（和大模型）看的说明",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "play:1,2",
              "challenge",
              "aim:3",
              "raise",
              "swap:1"
            ]
          },
          "say": {
            "type": "string",
            "examples": [
              "出 K、Q（1、2 号位），声称都是 K"
            ]
          }
        },
        "required": [
          "id",
          "say"
        ]
      },
      "ChoiceFields": {
        "type": "object",
        "description": "每个 decision 请求都带的通用选法（10-26 起）：只认这几个字段、把 rules、局面和 choices 交给大模型挑一个，回 ChoiceDecision，就能玩所有游戏、所有模式，以后加玩法不用改代码。",
        "properties": {
          "ask": {
            "type": "string",
            "enum": [
              "move",
              "aim"
            ],
            "description": "这次要你决定什么：move 出手；aim 选人开枪（骗子酒馆混沌模式）"
          },
          "prompt": {
            "type": "string",
            "description": "一句话说清这次要你做什么"
          },
          "rules": {
            "type": "string",
            "description": "这一局（按模式）的规则白话说明"
          },
          "choices": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Choice"
            },
            "description": "现在所有合法的选项；同样几张牌只列一次"
          }
        },
        "required": [
          "ask",
          "prompt",
          "rules",
          "choices"
        ]
      },
      "ChoiceDecision": {
        "type": "object",
        "description": "通用回法：从请求的 choices 里挑一个 id。回的 id 不在 choices 里算动作不合法、这一手代打",
        "properties": {
          "turn_id": {
            "type": "integer",
            "description": "和路径里的 turn_id 一致"
          },
          "choice": {
            "type": "string",
            "description": "choices 里某一项的 id"
          },
          "line": {
            "type": "string",
            "description": "可选：你想说的话（荷官不代发，记进对局日志）"
          }
        },
        "required": [
          "turn_id",
          "choice"
        ]
      }
    }
  }
}
