{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://elite.ewin-soft.com/doc/schema/gfi-v1.schema.json",
  "title": "GFI v1 event",
  "description": "elite 標準資料源介面（Game Feed Interface）v1 的單一事件。另有幾條 JSON Schema 無法表達的規則由伺服器檢查：seq 必須大於 prev；table.info 的 data.game 必須等於信封的 game；ts 必須是真實存在的時間。規格見 /doc 與 docs/10。",
  "$ref": "#/$defs/Event",
  "$defs": {
    "Timestamp": {"type": "string", "description": "UTC ISO-8601，含毫秒", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$"},
    "Card": {"type": "string", "description": "<點數><花色>：A 2…10 J Q K ＋ S H D C", "pattern": "^(A|[2-9]|10|J|Q|K)[SHDC]$"},
    "Game": {"enum": ["baccarat", "dragontiger", "niuniu"]},
    "TableStatus": {"enum": ["open", "paused", "maintenance", "closed"]},
    "Dealer": {
      "type": "object",
      "required": ["id", "name"],
      "properties": {
        "id": {"type": "string", "minLength": 1, "maxLength": 64},
        "name": {"type": "string", "minLength": 1, "maxLength": 128},
        "photoUrl": {"type": "string", "maxLength": 2048, "pattern": "^https://"}
      }
    },
    "VideoSource": {
      "type": "object",
      "required": ["protocol", "auth"],
      "properties": {
        "protocol": {"enum": ["webrtc", "flv", "hls", "agora", "whep"]},
        "url": {"type": "string", "maxLength": 2048},
        "quality": {"enum": ["sd", "hd"]},
        "auth": {"enum": ["none", "token", "referer"]}
      }
    },
    "Capabilities": {
      "type": "object",
      "required": ["cardByCard", "countdown", "snapshot"],
      "properties": {"cardByCard": {"type": "boolean"}, "countdown": {"type": "boolean"}, "snapshot": {"type": "boolean"}}
    },
    "BaccaratCards": {
      "type": "object",
      "propertyNames": {"enum": ["P1", "B1", "P2", "B2", "P3", "B3"]},
      "additionalProperties": {"$ref": "#/$defs/Card"}
    },
    "BaccaratResult": {
      "type": "object",
      "required": ["player", "banker", "winner", "playerPair", "bankerPair"],
      "properties": {
        "cards": {"$ref": "#/$defs/BaccaratCards"},
        "player": {"type": "integer", "minimum": 0, "maximum": 9},
        "banker": {"type": "integer", "minimum": 0, "maximum": 9},
        "winner": {"enum": ["P", "B", "T"]},
        "playerPair": {"type": "boolean"},
        "bankerPair": {"type": "boolean"},
        "cardCount": {"type": "integer", "minimum": 4, "maximum": 6}
      }
    },
    "BaccaratCompleteResult": {"required": ["cards"], "properties": {"cards": {"required": ["P1", "B1", "P2", "B2"]}}},
    "DragonTigerResult": {
      "type": "object",
      "required": ["winner"],
      "properties": {
        "cards": {
          "type": "object",
          "required": ["D", "T"],
          "propertyNames": {"enum": ["D", "T"]},
          "properties": {"D": {"$ref": "#/$defs/Card"}, "T": {"$ref": "#/$defs/Card"}}
        },
        "winner": {"enum": ["D", "T", "TIE"]}
      }
    },
    "TableInfoData": {
      "type": "object",
      "required": ["name", "game", "capabilities"],
      "properties": {
        "name": {"type": "string", "minLength": 1, "maxLength": 128},
        "game": {"$ref": "#/$defs/Game"},
        "decks": {"type": "integer", "minimum": 1, "maximum": 16},
        "dealer": {"$ref": "#/$defs/Dealer"},
        "video": {"type": "array", "maxItems": 8, "items": {"$ref": "#/$defs/VideoSource"}},
        "capabilities": {"$ref": "#/$defs/Capabilities"}
      }
    },
    "TableStatusData": {
      "type": "object",
      "required": ["status"],
      "properties": {"status": {"$ref": "#/$defs/TableStatus"}, "reason": {"type": "string", "maxLength": 500}}
    },
    "TableSnapshotData": {
      "type": "object",
      "required": ["status", "phase", "history", "gap"],
      "properties": {
        "status": {"$ref": "#/$defs/TableStatus"},
        "phase": {"enum": ["idle", "betting", "closed", "result"]},
        "shoe": {"type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,64}$"},
        "round": {"type": "integer", "minimum": 1, "maximum": 100000},
        "roundKey": {"type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,128}$"},
        "closesAt": {"$ref": "#/$defs/Timestamp"},
        "history": {
          "type": "array",
          "maxItems": 200,
          "items": {
            "type": "object",
            "required": ["round", "result"],
            "properties": {"round": {"type": "integer", "minimum": 1, "maximum": 100000}, "result": {"type": "object"}}
          }
        },
        "gap": {"type": "boolean"}
      }
    },
    "ShoeStartData": {"type": "object", "properties": {"decks": {"type": "integer", "minimum": 1, "maximum": 16}}},
    "ShoeEndData": {"type": "object", "required": ["rounds"], "properties": {"rounds": {"type": "integer", "minimum": 0, "maximum": 100000}}},
    "RoundOpenData": {
      "type": "object",
      "properties": {
        "closesAt": {"$ref": "#/$defs/Timestamp"},
        "betSeconds": {"type": "integer", "minimum": 1, "maximum": 600},
        "lastInShoe": {"type": "boolean"},
        "dealer": {"$ref": "#/$defs/Dealer"}
      }
    },
    "CardData": {"type": "object", "required": ["pos", "card"], "properties": {"card": {"$ref": "#/$defs/Card"}, "faceDown": {"type": "boolean"}}},
    "RoundResultData": {
      "type": "object",
      "required": ["rev", "complete", "result"],
      "properties": {"rev": {"const": 1}, "complete": {"type": "boolean"}, "result": {"type": "object"}}
    },
    "RoundCorrectData": {
      "type": "object",
      "required": ["rev", "complete", "result", "reason"],
      "properties": {
        "rev": {"type": "integer", "minimum": 2, "maximum": 9007199254740991},
        "complete": {"type": "boolean"},
        "result": {"type": "object"},
        "reason": {"type": "string", "minLength": 1, "maxLength": 500}
      }
    },
    "RoundCancelData": {
      "type": "object",
      "required": ["reason"],
      "properties": {
        "reason": {"enum": ["misdeal", "dealer_error", "device_error", "table_closed", "other"]},
        "note": {"type": "string", "maxLength": 500}
      }
    },
    "DealerChangeData": {"type": "object", "required": ["dealer"], "properties": {"dealer": {"$ref": "#/$defs/Dealer"}}},
    "Event": {
      "type": "object",
      "required": ["v", "type", "table", "seq", "prev", "ts", "data"],
      "properties": {
        "v": {"const": 1},
        "type": {"type": "string", "pattern": "^[a-z][a-z0-9_.]{0,63}$"},
        "table": {"type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,64}$"},
        "seq": {"type": "integer", "minimum": 1, "maximum": 9007199254740991},
        "prev": {"type": "integer", "minimum": 0, "maximum": 9007199254740991},
        "ts": {"$ref": "#/$defs/Timestamp"},
        "game": {"$ref": "#/$defs/Game"},
        "shoe": {"type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,64}$"},
        "round": {"type": "integer", "minimum": 1, "maximum": 100000},
        "roundKey": {"type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,128}$"},
        "data": {"type": "object"}
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "type": {
                "enum": [
                  "table.info",
                  "table.status",
                  "table.snapshot",
                  "shoe.start",
                  "shoe.end",
                  "round.open",
                  "round.close",
                  "card",
                  "reveal",
                  "round.result",
                  "round.correct",
                  "round.cancel",
                  "dealer.change"
                ]
              }
            },
            "required": ["type"]
          },
          "then": {"required": ["game"]}
        },
        {
          "if": {
            "properties": {
              "type": {
                "enum": ["shoe.start", "shoe.end", "round.open", "round.close", "card", "reveal", "round.result", "round.correct", "round.cancel"]
              }
            },
            "required": ["type"]
          },
          "then": {"required": ["shoe"]}
        },
        {
          "if": {
            "properties": {"type": {"enum": ["round.open", "round.close", "card", "reveal", "round.result", "round.correct", "round.cancel"]}},
            "required": ["type"]
          },
          "then": {"required": ["round", "roundKey"]}
        },
        {
          "if": {"properties": {"type": {"const": "table.info"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/TableInfoData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "table.status"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/TableStatusData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "table.snapshot"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/TableSnapshotData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "shoe.start"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/ShoeStartData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "shoe.end"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/ShoeEndData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "round.open"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/RoundOpenData"}}}
        },
        {"if": {"properties": {"type": {"const": "card"}}, "required": ["type"]}, "then": {"properties": {"data": {"$ref": "#/$defs/CardData"}}}},
        {
          "if": {"properties": {"type": {"const": "reveal"}}, "required": ["type"]},
          "then": {"required": ["game"], "properties": {"game": {"const": "niuniu"}, "data": {"$ref": "#/$defs/RevealData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "round.result"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/RoundResultData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "round.correct"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/RoundCorrectData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "round.cancel"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/RoundCancelData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "dealer.change"}}, "required": ["type"]},
          "then": {"properties": {"data": {"$ref": "#/$defs/DealerChangeData"}}}
        },
        {
          "if": {"properties": {"type": {"const": "card"}, "game": {"const": "dragontiger"}}, "required": ["type", "game"]},
          "then": {"properties": {"data": {"properties": {"pos": {"enum": ["D", "T"]}}}}},
          "else": {
            "if": {"properties": {"type": {"const": "card"}, "game": {"const": "niuniu"}}, "required": ["type", "game"]},
            "then": {
              "properties": {
                "data": {
                  "properties": {
                    "pos": {
                      "enum": [
                        "F",
                        "B-1",
                        "B-2",
                        "B-3",
                        "B-4",
                        "B-5",
                        "P1-1",
                        "P1-2",
                        "P1-3",
                        "P1-4",
                        "P1-5",
                        "P2-1",
                        "P2-2",
                        "P2-3",
                        "P2-4",
                        "P2-5",
                        "P3-1",
                        "P3-2",
                        "P3-3",
                        "P3-4",
                        "P3-5"
                      ]
                    }
                  },
                  "if": {"properties": {"pos": {"const": "F"}}, "required": ["pos"]},
                  "then": {"properties": {"faceDown": {"const": false}}}
                }
              }
            },
            "else": {
              "if": {"properties": {"type": {"const": "card"}}, "required": ["type"]},
              "then": {"properties": {"data": {"properties": {"pos": {"enum": ["P1", "B1", "P2", "B2", "P3", "B3"]}}}}}
            }
          }
        },
        {
          "if": {"properties": {"type": {"enum": ["round.result", "round.correct"]}, "game": {"const": "baccarat"}}, "required": ["type", "game"]},
          "then": {
            "properties": {
              "data": {
                "properties": {"result": {"$ref": "#/$defs/BaccaratResult"}},
                "if": {"properties": {"complete": {"const": true}}, "required": ["complete"]},
                "then": {"properties": {"result": {"$ref": "#/$defs/BaccaratCompleteResult"}}}
              }
            }
          }
        },
        {
          "if": {"properties": {"type": {"enum": ["round.result", "round.correct"]}, "game": {"const": "dragontiger"}}, "required": ["type", "game"]},
          "then": {
            "properties": {
              "data": {
                "properties": {"result": {"$ref": "#/$defs/DragonTigerResult"}},
                "if": {"properties": {"complete": {"const": true}}, "required": ["complete"]},
                "then": {"properties": {"result": {"required": ["cards"]}}}
              }
            }
          }
        },
        {
          "if": {"properties": {"type": {"const": "table.snapshot"}, "game": {"const": "baccarat"}}, "required": ["type", "game"]},
          "then": {"properties": {"data": {"properties": {"history": {"items": {"properties": {"result": {"$ref": "#/$defs/BaccaratResult"}}}}}}}}
        },
        {
          "if": {"properties": {"type": {"const": "table.snapshot"}, "game": {"const": "dragontiger"}}, "required": ["type", "game"]},
          "then": {"properties": {"data": {"properties": {"history": {"items": {"properties": {"result": {"$ref": "#/$defs/DragonTigerResult"}}}}}}}}
        },
        {
          "if": {"properties": {"type": {"enum": ["round.result", "round.correct"]}, "game": {"const": "niuniu"}}, "required": ["type", "game"]},
          "then": {
            "properties": {
              "data": {
                "properties": {"result": {"$ref": "#/$defs/NiuniuResult"}},
                "if": {"properties": {"complete": {"const": true}}, "required": ["complete"]},
                "then": {"properties": {"result": {"$ref": "#/$defs/NiuniuCompleteResult"}}}
              }
            }
          }
        },
        {
          "if": {"properties": {"type": {"const": "table.snapshot"}, "game": {"const": "niuniu"}}, "required": ["type", "game"]},
          "then": {"properties": {"data": {"properties": {"history": {"items": {"properties": {"result": {"$ref": "#/$defs/NiuniuResult"}}}}}}}}
        }
      ]
    },
    "Hello": {
      "type": "object",
      "required": ["op", "v", "source", "tables"],
      "properties": {
        "op": {"const": "hello"},
        "v": {"const": 1},
        "source": {"type": "string", "pattern": "^[a-z][a-z0-9_-]{1,31}$"},
        "agent": {"type": "string", "maxLength": 128},
        "tables": {"type": "array", "maxItems": 1000, "items": {"type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,64}$"}},
        "capabilities": {"allOf": [{"$ref": "#/$defs/Capabilities"}], "properties": {"replayHours": {"type": "integer", "minimum": 0}}}
      }
    },
    "Events": {
      "type": "object",
      "required": ["op", "events"],
      "properties": {"op": {"const": "events"}, "events": {"type": "array", "maxItems": 500, "items": {"$ref": "#/$defs/Event"}}}
    },
    "Heartbeat": {
      "type": "object",
      "required": ["op", "ts"],
      "properties": {
        "op": {"const": "heartbeat"},
        "ts": {"$ref": "#/$defs/Timestamp"},
        "tables": {"type": "object", "additionalProperties": {"enum": ["ok", "degraded", "down"]}}
      }
    },
    "Welcome": {
      "type": "object",
      "required": ["op", "v", "session", "heartbeat", "maxBatch", "maxInflight", "resume", "serverTime"],
      "properties": {
        "op": {"const": "welcome"},
        "v": {"const": 1},
        "session": {"type": "string"},
        "heartbeat": {"type": "integer"},
        "maxBatch": {"type": "integer"},
        "maxInflight": {"type": "integer"},
        "resume": {"type": "object", "additionalProperties": {"type": "integer", "minimum": 0}},
        "serverTime": {"$ref": "#/$defs/Timestamp"}
      }
    },
    "Ack": {
      "type": "object",
      "required": ["op", "acks"],
      "properties": {"op": {"const": "ack"}, "acks": {"type": "object", "additionalProperties": {"type": "integer", "minimum": 0}}}
    },
    "Nack": {
      "type": "object",
      "required": ["op", "code"],
      "properties": {
        "op": {"const": "nack"},
        "code": {
          "enum": [
            "UNAUTHORIZED",
            "SOURCE_DISABLED",
            "UNSUPPORTED_VERSION",
            "TABLE_NOT_ALLOWED",
            "INVALID_EVENT",
            "SEQ_GAP",
            "SEQ_CONFLICT",
            "RESULT_CONFLICT",
            "RESULT_INCONSISTENT",
            "STREAM_TAKEN_OVER",
            "RATE_LIMITED"
          ]
        },
        "table": {"type": "string"},
        "seq": {"type": "integer"},
        "message": {"type": "string"},
        "accepted": {"type": "boolean"}
      }
    },
    "Resync": {
      "type": "object",
      "required": ["op", "table", "from"],
      "properties": {"op": {"const": "resync"}, "table": {"type": "string"}, "from": {"type": "integer", "minimum": 0}}
    },
    "NiuniuCards": {
      "type": "object",
      "description": "牛牛牌位：頭牌 F、莊 B-1…B-5、閒 P1-1…P3-5",
      "maxProperties": 21,
      "propertyNames": {
        "enum": [
          "F",
          "B-1",
          "B-2",
          "B-3",
          "B-4",
          "B-5",
          "P1-1",
          "P1-2",
          "P1-3",
          "P1-4",
          "P1-5",
          "P2-1",
          "P2-2",
          "P2-3",
          "P2-4",
          "P2-5",
          "P3-1",
          "P3-2",
          "P3-3",
          "P3-4",
          "P3-5"
        ]
      },
      "additionalProperties": {"$ref": "#/$defs/Card"}
    },
    "NiuniuResult": {
      "type": "object",
      "required": ["winners"],
      "description": "牛牛結果（docs/22 §6.1）：hands 0 無牛、1–9 牛幾、10 牛牛、11 五花牛、12 炸彈、13 五小牛；winners 每一家閒對莊（P 閒贏、B 莊贏）。21 張不重複、牌型與輸贏是否符合牌面由伺服器檢查（RESULT_INCONSISTENT）。",
      "properties": {
        "cards": {"$ref": "#/$defs/NiuniuCards"},
        "hands": {
          "type": "object",
          "propertyNames": {"enum": ["B", "P1", "P2", "P3"]},
          "additionalProperties": {"type": "integer", "minimum": 0, "maximum": 13}
        },
        "winners": {
          "type": "object",
          "required": ["P1", "P2", "P3"],
          "propertyNames": {"enum": ["P1", "P2", "P3"]},
          "additionalProperties": {"enum": ["P", "B"]}
        }
      }
    },
    "NiuniuCompleteResult": {
      "required": ["cards"],
      "properties": {
        "cards": {
          "required": [
            "F",
            "B-1",
            "B-2",
            "B-3",
            "B-4",
            "B-5",
            "P1-1",
            "P1-2",
            "P1-3",
            "P1-4",
            "P1-5",
            "P2-1",
            "P2-2",
            "P2-3",
            "P2-4",
            "P2-5",
            "P3-1",
            "P3-2",
            "P3-3",
            "P3-4",
            "P3-5"
          ]
        }
      }
    },
    "RevealData": {
      "type": "object",
      "required": ["hands"],
      "properties": {"hands": {"type": "array", "minItems": 1, "maxItems": 4, "uniqueItems": true, "items": {"enum": ["B", "P1", "P2", "P3"]}}}
    }
  }
}
