{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://sashite.dev/schemas/pcn/1.0.0/schema.json",
  "title": "Portable Chess Notation (PCN) v1.0.0",
  "description": "JSON format for game records using PMN, FEEN, SNN, CGSN, and integrated time control",
  "type": "object",
  "required": ["setup"],
  "properties": {
    "meta": {
      "type": "object",
      "description": "Game metadata (standard fields + custom fields allowed)",
      "default": {},
      "properties": {
        "name": {
          "type": "string",
          "description": "Game or opening name"
        },
        "event": {
          "type": "string",
          "description": "Tournament or event name"
        },
        "location": {
          "type": "string",
          "description": "Physical or virtual venue"
        },
        "round": {
          "type": "integer",
          "minimum": 1,
          "description": "Round number in tournament"
        },
        "started_at": {
          "type": "string",
          "format": "date-time",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$",
          "description": "Game start timestamp in UTC (ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ)"
        },
        "href": {
          "type": "string",
          "format": "uri",
          "pattern": "^https?://",
          "description": "Reference URL"
        }
      },
      "additionalProperties": true
    },
    "sides": {
      "type": "object",
      "description": "Player information",
      "default": {},
      "properties": {
        "first": {
          "$ref": "#/$defs/player",
          "description": "First player",
          "default": {}
        },
        "second": {
          "$ref": "#/$defs/player",
          "description": "Second player",
          "default": {}
        }
      },
      "additionalProperties": false
    },
    "setup": {
      "type": "string",
      "description": "Initial position in FEEN format",
      "pattern": "^.+\\s+.+\\s+[A-Za-z]+/[A-Za-z]+$"
    },
    "moves": {
      "type": "array",
      "description": "Sequence of [PMN, seconds] tuples with time tracking",
      "default": [],
      "items": {
        "$ref": "#/$defs/move_tuple"
      }
    },
    "draw_offered_by": {
      "type": ["string", "null"],
      "enum": ["first", "second", null],
      "default": null,
      "description": "Player who has offered a draw"
    },
    "status": {
      "type": ["string", "null"],
      "enum": [
        "check",
        "stale",
        "checkmate",
        "stalemate",
        "nomove",
        "bareking",
        "mareking",
        "insufficient",
        "resignation",
        "illegalmove",
        "timelimit",
        "movelimit",
        "repetition",
        "agreement",
        null
      ],
      "default": null,
      "description": "Observable game status (CGSN value)"
    },
    "winner": {
      "type": ["string", "null"],
      "enum": ["first", "second", "none", null],
      "default": null,
      "description": "Explicit competitive outcome of the game"
    }
  },
  "additionalProperties": false,
  "$defs": {
    "player": {
      "type": "object",
      "properties": {
        "style": {
          "type": "string",
          "pattern": "^([A-Z]+|[a-z]+)$",
          "description": "Style name in SNN format"
        },
        "name": {
          "type": "string",
          "description": "Player name or identifier"
        },
        "elo": {
          "type": "integer",
          "minimum": 0,
          "description": "Elo rating"
        },
        "periods": {
          "type": "array",
          "description": "Time control periods (empty array = no time control)",
          "default": [],
          "items": {
            "$ref": "#/$defs/period"
          }
        }
      },
      "additionalProperties": false
    },
    "period": {
      "type": "object",
      "required": ["time"],
      "properties": {
        "time": {
          "type": "integer",
          "minimum": 0,
          "description": "Time budget in seconds"
        },
        "moves": {
          "oneOf": [
            {"type": "null"},
            {"type": "integer", "minimum": 1}
          ],
          "default": null,
          "description": "Move quota: null=bank, 1=per-move, ≥2=quota"
        },
        "inc": {
          "type": "integer",
          "minimum": 0,
          "default": 0,
          "description": "Fischer increment in seconds"
        }
      },
      "additionalProperties": false
    },
    "move_tuple": {
      "type": "array",
      "description": "Move tuple: [PMN action, time spent in seconds]",
      "prefixItems": [
        {
          "$ref": "#/$defs/pmn_action",
          "description": "PMN action string"
        },
        {
          "type": "number",
          "minimum": 0.0,
          "description": "Time spent in seconds (floating-point)"
        }
      ],
      "minItems": 2,
      "maxItems": 2,
      "additionalItems": false
    },
    "cell_coordinate": {
      "type": "string",
      "pattern": "^[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?$",
      "description": "CELL coordinate format"
    },
    "hand_location": {
      "type": "string",
      "pattern": "^\\*$",
      "description": "HAND location marker"
    },
    "location": {
      "oneOf": [
        {"$ref": "#/$defs/cell_coordinate"},
        {"$ref": "#/$defs/hand_location"}
      ],
      "description": "Board coordinate (CELL) or reserve location (HAND)"
    },
    "epin_piece": {
      "type": "string",
      "pattern": "^[-+]?[A-Za-z]'?$",
      "description": "Extended Piece Identifier Notation (EPIN) format"
    },
    "pmn_action": {
      "type": "string",
      "description": "Portable Move Notation (PMN) string",
      "oneOf": [
        {
          "pattern": "^\\.\\.\\.+$",
          "description": "Pass action"
        },
        {
          "pattern": "^[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?-[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?(?:=[-+]?[A-Za-z]'?)?$",
          "description": "Movement to empty square: <src>-<dst>[=<piece>]"
        },
        {
          "pattern": "^[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?\\+[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?(?:=[-+]?[A-Za-z]'?)?$",
          "description": "Capture movement: <src>+<dst>[=<piece>]"
        },
        {
          "pattern": "^[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?~[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?(?:=[-+]?[A-Za-z]'?)?$",
          "description": "Special movement: <src>~<dst>[=<piece>]"
        },
        {
          "pattern": "^\\+[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?$",
          "description": "Static capture: +<square>"
        },
        {
          "pattern": "^[-+]?[A-Za-z]'?\\*[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?(?:=[-+]?[A-Za-z]'?)?$",
          "description": "Drop to empty with piece: <piece>*<dst>[=<piece>]"
        },
        {
          "pattern": "^\\*[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?(?:=[-+]?[A-Za-z]'?)?$",
          "description": "Drop to empty inferred: *<dst>[=<piece>]"
        },
        {
          "pattern": "^[-+]?[A-Za-z]'?\\.[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?(?:=[-+]?[A-Za-z]'?)?$",
          "description": "Drop with capture: <piece>.<dst>[=<piece>]"
        },
        {
          "pattern": "^\\.[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?(?:=[-+]?[A-Za-z]'?)?$",
          "description": "Drop with capture inferred: .<dst>[=<piece>]"
        },
        {
          "pattern": "^[a-z]+(?:[1-9]\\d*[A-Z]+[a-z]+)*(?:[1-9]\\d*[A-Z]*)?=[-+]?[A-Za-z]'?$",
          "description": "In-place modification: <square>=<piece>"
        }
      ]
    }
  },
  "examples": [
    {
      "description": "Minimal valid PCN",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Chess starting position",
      "value": {
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/c"
      }
    },
    {
      "description": "Chess game with tuple moves and winner",
      "value": {
        "sides": {
          "first": {"style": "CHESS", "name": "Alice", "elo": 2100},
          "second": {"style": "chess", "name": "Bob", "elo": 2050}
        },
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e2-e4", 0.0],
          ["e7-e5", 0.0],
          ["g1-f3", 0.0],
          ["b8-c6", 0.0],
          ["f1-c4", 0.0],
          ["f8-c5", 0.0]
        ],
        "status": "resignation",
        "winner": "first"
      }
    },
    {
      "description": "Blitz game with Fischer time control and draw",
      "value": {
        "meta": {
          "event": "Blitz Tournament",
          "started_at": "2025-01-27T14:30:00Z"
        },
        "sides": {
          "first": {
            "name": "Alice",
            "elo": 2100,
            "periods": [
              {"time": 300, "inc": 3}
            ]
          },
          "second": {
            "name": "Bob",
            "elo": 2050,
            "periods": [
              {"time": 300, "inc": 3}
            ]
          }
        },
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e2-e4", 8.0],
          ["e7-e5", 12.0],
          ["g1-f3", 15.0],
          ["b8-c6", 5.0]
        ],
        "status": "repetition",
        "winner": "none"
      }
    },
    {
      "description": "Game with time forfeit and explicit winner",
      "value": {
        "meta": {
          "event": "Club Match"
        },
        "sides": {
          "first": {
            "style": "CHESS",
            "name": "Alice",
            "periods": [{"time": 60}]
          },
          "second": {
            "style": "chess",
            "name": "Bob",
            "periods": [{"time": 60}]
          }
        },
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e2-e4", 15.0],
          ["e7-e5", 20.0],
          ["g1-f3", 45.0]
        ],
        "status": "timelimit",
        "winner": "second"
      }
    },
    {
      "description": "Game with draw offer and agreement",
      "value": {
        "meta": {
          "event": "Club Match",
          "round": 2
        },
        "sides": {
          "first": {
            "style": "CHESS",
            "name": "Alice",
            "elo": 2100
          },
          "second": {
            "style": "chess",
            "name": "Bob",
            "elo": 2050
          }
        },
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e2-e4", 8.0],
          ["e7-e5", 12.0],
          ["g1-f3", 15.0],
          ["b8-c6", 5.0],
          ["f1-c4", 9.0]
        ],
        "draw_offered_by": "first",
        "status": "agreement",
        "winner": "none"
      }
    },
    {
      "description": "Game with byoyomi time control",
      "value": {
        "sides": {
          "first": {
            "name": "Sente",
            "periods": [
              {"time": 600},
              {"time": 30, "moves": 1}
            ]
          },
          "second": {
            "name": "Gote",
            "periods": [
              {"time": 600},
              {"time": 30, "moves": 1}
            ]
          }
        },
        "setup": "lnsgk^gsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGK^GSNL / S/s",
        "moves": [
          ["c3-c4", 5.2],
          ["g7-g6", 8.0]
        ]
      }
    },
    {
      "description": "Game with Canadian time control",
      "value": {
        "sides": {
          "first": {
            "periods": [
              {"time": 3600},
              {"time": 300, "moves": 25}
            ]
          },
          "second": {
            "periods": [
              {"time": 3600},
              {"time": 300, "moves": 25}
            ]
          }
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [
          ["...", 0.0]
        ]
      }
    },
    {
      "description": "Asymmetric time control (handicap)",
      "value": {
        "sides": {
          "first": {
            "name": "Expert",
            "periods": [{"time": 600}]
          },
          "second": {
            "name": "Beginner",
            "periods": [{"time": 1800}]
          }
        },
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e2-e4", 15.0],
          ["e7-e5", 45.0]
        ]
      }
    },
    {
      "description": "Chess with castling (special movement)",
      "value": {
        "setup": "-r2qk^bn-r/+p+p+p+p1+p+p+p/2n5/1Bb1p3/4P3/5N2/+P+P+P+P1+P+P+P/-RNBQK^2+R / C/c",
        "moves": [
          ["e1~g1", 0.0]
        ]
      }
    },
    {
      "description": "Chess with en passant (special movement)",
      "value": {
        "setup": "-rnbqk^bn-r/+p+p+p1+p1+p+p/8/3pP-p2/8/8/+P+P+P+P1+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e5~f6", 0.0]
        ]
      }
    },
    {
      "description": "Chess with promotion",
      "value": {
        "setup": "4k^3/P7/4K^3/8/8/8/8/8 / C/c",
        "moves": [
          ["a7-a8=R", 0.0]
        ]
      }
    },
    {
      "description": "Chess with capture",
      "value": {
        "setup": "-rnbqk^bn-r/+p+p+p1+p+p+p+p/8/3p4/4P3/8/+P+P+P+P1+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e4+d5", 0.0]
        ]
      }
    },
    {
      "description": "Shogi with drop",
      "value": {
        "sides": {
          "first": {"style": "SHOGI"},
          "second": {"style": "shogi"}
        },
        "setup": "lnsgk^g1nl/1r5s1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/7R1/LNSGK^GSNL B/b S/s",
        "moves": [
          ["B*f5", 0.0]
        ]
      }
    },
    {
      "description": "Game with pass move",
      "value": {
        "setup": "8/8/4k^3/8/8/4K^3/8/8 / U/u",
        "moves": [
          ["...", 0.0]
        ]
      }
    },
    {
      "description": "Position without moves",
      "value": {
        "meta": {
          "name": "Endgame Study"
        },
        "setup": "4k^3/8/8/8/8/8/4P3/4K^3 / C/c"
      }
    },
    {
      "description": "Game ending in checkmate with winner",
      "value": {
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/4P3/8/+P+P+P+P1+P+P+P/-RNBQK^BN-R / c/C",
        "moves": [
          ["c7-c5", 0.0]
        ],
        "status": "checkmate",
        "winner": "second"
      }
    },
    {
      "description": "Cross-style game (Chess vs Makruk)",
      "value": {
        "sides": {
          "first": {"style": "CHESS", "name": "Western Champion"},
          "second": {"style": "makruk", "name": "Thai Champion"}
        },
        "setup": "rnsmk^snr/8/pppppppp/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/m",
        "moves": [
          ["e2-e4", 0.0],
          ["d6-d5", 0.0]
        ]
      }
    },
    {
      "description": "Game with full metadata and winner",
      "value": {
        "meta": {
          "name": "Italian Game",
          "event": "World Championship",
          "location": "Dubai, UAE",
          "round": 5,
          "started_at": "2025-01-27T14:30:00Z",
          "href": "https://example.com/game/12345"
        },
        "sides": {
          "first": {"style": "CHESS", "name": "Magnus Carlsen", "elo": 2830},
          "second": {"style": "chess", "name": "Fabiano Caruana", "elo": 2820}
        },
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e2-e4", 8.0],
          ["e7-e5", 12.0],
          ["g1-f3", 15.0],
          ["b8-c6", 5.0],
          ["f1-c4", 120.0],
          ["f8-c5", 180.0]
        ],
        "status": "resignation",
        "winner": "first"
      }
    },
    {
      "description": "In-place modification",
      "value": {
        "setup": "8/8/8/8/4P3/8/8/8 / C/c",
        "moves": [
          ["e4=+P", 0.0]
        ]
      }
    },
    {
      "description": "Static capture",
      "value": {
        "setup": "8/8/8/4p3/8/8/8/8 / C/c",
        "moves": [
          ["+e5", 0.0]
        ]
      }
    },
    {
      "description": "Game with illegal move and winner",
      "value": {
        "sides": {
          "first": {"style": "CHESS"},
          "second": {"style": "chess"}
        },
        "setup": "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / C/c",
        "moves": [
          ["e2~e5", 0.2]
        ],
        "status": "illegalmove",
        "winner": "second"
      }
    },
    {
      "description": "Position in check",
      "value": {
        "setup": "4k^3/8/8/8/8/8/4R3/4K^3 / c/C",
        "moves": [],
        "status": "check"
      }
    },
    {
      "description": "Position in stale (not in check)",
      "value": {
        "setup": "4k^3/8/8/8/8/8/8/R3K^3 / c/C",
        "moves": [],
        "status": "stale"
      }
    },
    {
      "description": "Game ending in nomove",
      "value": {
        "setup": "8/8/8/3ppp2/3PKP2/3PPP2/8/8 / c/C",
        "moves": [],
        "status": "nomove"
      }
    }
  ],
  "counterExamples": [
    {
      "description": "Missing required setup field",
      "value": {
        "moves": [["e2-e4", 0.0]]
      }
    },
    {
      "description": "Invalid move format - missing time (must be tuple)",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": ["e2-e4"]
      }
    },
    {
      "description": "Invalid move tuple - only one element",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [["e2-e4"]]
      }
    },
    {
      "description": "Invalid move tuple - more than two elements",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [["e2-e4", 0.0, "extra"]]
      }
    },
    {
      "description": "Invalid time value - negative",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [["e2-e4", -5.0]]
      }
    },
    {
      "description": "Invalid time value - string instead of number",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [["e2-e4", "5.0"]]
      }
    },
    {
      "description": "Invalid PMN format - wrong separator (comma instead of dash)",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [["e2,e4", 0.0]]
      }
    },
    {
      "description": "Invalid PMN format - missing operator",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [["e2e4", 0.0]]
      }
    },
    {
      "description": "Invalid period - missing required time field",
      "value": {
        "sides": {
          "first": {
            "periods": [{"moves": 1, "inc": 0}]
          }
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Invalid period - negative time",
      "value": {
        "sides": {
          "first": {
            "periods": [{"time": -300}]
          }
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Invalid period - moves = 0",
      "value": {
        "sides": {
          "first": {
            "periods": [{"time": 300, "moves": 0}]
          }
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Invalid period - negative increment",
      "value": {
        "sides": {
          "first": {
            "periods": [{"time": 300, "inc": -3}]
          }
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Invalid FEEN format",
      "value": {
        "setup": "invalid feen string"
      }
    },
    {
      "description": "Invalid status value",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "status": "win"
      }
    },
    {
      "description": "Invalid status value - removed in_progress",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "status": "in_progress"
      }
    },
    {
      "description": "Invalid winner value",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "winner": "white"
      }
    },
    {
      "description": "Invalid draw_offered_by value",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "draw_offered_by": "white"
      }
    },
    {
      "description": "Invalid SNN format - mixed case",
      "value": {
        "sides": {
          "first": {"style": "Chess"}
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Invalid timestamp format - missing Z suffix",
      "value": {
        "meta": {
          "started_at": "2025-01-27T14:30:00"
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Invalid timestamp format - wrong separator",
      "value": {
        "meta": {
          "started_at": "2025-01-27 14:30:00Z"
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Extra root property",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "extra": "not allowed"
      }
    },
    {
      "description": "Invalid CELL coordinate - starts with number",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [["2e-4e", 0.0]]
      }
    },
    {
      "description": "Invalid CELL coordinate - uppercase without prior numeric",
      "value": {
        "setup": "8/8/8/8/8/8/8/8 / U/u",
        "moves": [["eA-fA", 0.0]]
      }
    },
    {
      "description": "Invalid elo - must be non-negative",
      "value": {
        "sides": {
          "first": {"elo": -100}
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Invalid round - must be positive",
      "value": {
        "meta": {
          "round": 0
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Extra property in player object",
      "value": {
        "sides": {
          "first": {"style": "CHESS", "extra": "not allowed"}
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    },
    {
      "description": "Extra property in period object",
      "value": {
        "sides": {
          "first": {
            "periods": [{"time": 300, "extra": "not allowed"}]
          }
        },
        "setup": "8/8/8/8/8/8/8/8 / U/u"
      }
    }
  ]
}
