{
  "openapi": "3.1.1",
  "info": {
    "title": "Playcards API",
    "version": "1.0.0",
    "summary": "Deterministic tools for people and AI agents.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://playcards.usefulweb.site"
    }
  ],
  "paths": {
    "/api/v1/tools": {
      "get": {
        "operationId": "listTools",
        "summary": "Catalog of all tools",
        "responses": {
          "200": {
            "description": "Tool list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/{id}": {
      "get": {
        "operationId": "getTool",
        "summary": "Definition of one tool",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "playcards-skat-game-value",
                "playcards-doppelkopf-score",
                "playcards-blackjack-strategy"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/playcards-skat-game-value/calculate": {
      "post": {
        "operationId": "calculatePlaycardsSkatGameValue",
        "summary": "Skat game value from contract, matadors, Hand, Schneider, Schwarz and Ouvert — with overbid check.",
        "description": "Works out the value of a Skat game by the International Skat Order: base value of the suit or Grand times the multiplier (matadors with or without, game, Hand, Schneider, announced, Schwarz, announced, Ouvert), or the fixed Null values 23/35/46/59. Enter your bid to see whether the game covers it, the highest bid it covers and — with the declarer’s card points — whether the game is won and what it scores. The Playcards app uses the same engine for its score sheet.\n\nMethodology:\n- Suit games: base value Diamonds 9, Hearts 10, Spades 11, Clubs 12; Grand 24. Multiplier = matadors (with or without) + 1 for the game + 1 each for Hand, Schneider, Schneider announced, Schwarz, Schwarz announced and Ouvert (ISkO 4.1–4.4). Announcing Schwarz implies Schneider announced; Ouvert implies both.\n- Null games have fixed values: Null 23, Null Hand 35, Null Ouvert 46, Null Ouvert Hand 59.\n- A game whose value is below the bid is lost (overbid); it then counts at least the smallest multiple of its base value that reaches the bid (ISkO 5.6).\n- With card points: the declarer wins with 61 or more (Schneider: 90+, an announced Schneider or Schwarz must be reached). Score sheet: won +value, lost −2 × value; Seeger-Fabian adds +50 / −50 and 40 for each defender when the declarer loses (three-player table).",
        "tags": [
          "apps"
        ],
        "x-mcp-tool": "playcards_skat_game_value",
        "x-owner-app": "playcards",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaycardsSkatGameValueInput"
              },
              "examples": {
                "example1": {
                  "summary": "Clubs with 2, Hand — bid 48",
                  "value": {
                    "game": "C",
                    "matadorsWith": "with",
                    "matadors": 2,
                    "hand": true,
                    "bid": 48
                  }
                },
                "example2": {
                  "summary": "Grand without 1, Schneider, 92 points",
                  "value": {
                    "game": "grand",
                    "matadorsWith": "without",
                    "matadors": 1,
                    "schneider": true,
                    "declarerPoints": "92",
                    "bid": 72
                  }
                },
                "example3": {
                  "summary": "Overbid: Hearts with 1 after bidding 36",
                  "value": {
                    "game": "H",
                    "matadorsWith": "with",
                    "matadors": 1,
                    "bid": 36,
                    "declarerPoints": "71"
                  }
                },
                "example4": {
                  "summary": "Null Ouvert Hand",
                  "value": {
                    "game": "null",
                    "hand": true,
                    "ouvert": true,
                    "bid": 59
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaycardsSkatGameValueResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/playcards-doppelkopf-score/calculate": {
      "post": {
        "operationId": "calculatePlaycardsDoppelkopfScore",
        "summary": "Doppelkopf game points from card points, announcements and special points (DDV rules).",
        "description": "Scores one game of Doppelkopf by the tournament rules of the Deutscher Doppelkopf-Verband: who won (121 for Re, 120 for Kontra, shifted by announcements and Absagen), the plus-minus points for winning, against the Queens of Clubs, under 90/60/30, Schwarz, Re/Kontra (+2 each), every Absage, reaching 120/90/60/30 against an Absage, and the special points Fox caught, Karlchen and Doppelkopf in normal games. A soloist scores three times. The Playcards app scores every game with the same function.\n\nMethodology:\n- Winner (DDV 7.1): Re needs 121 card points, Kontra 120; if only Kontra was announced, Re wins with 120 and Kontra needs 121. A party that said keine 90/60/30 needs 151/181/211, after Schwarz every trick; the other party then wins with 90/60/30 or one trick. If both parties fail their Absagen, nobody wins.\n- Points (DDV 7.2): won 1, against the Queens of Clubs 1 (Kontra wins a normal game), under 90/60/30 and Schwarz 1 each, Re and Kontra announced 2 each, every Absage 1, reaching 120/90/60/30 against an Absage 1 each (also without a winner). Special points (normal games only): every fox caught, Karlchen and every Doppelkopf 1 for the party that made it; they are set off against each other.\n- Plus-minus scoring: each Re player gets the value, each Kontra player its negative; a soloist gets three times the value.",
        "tags": [
          "apps"
        ],
        "x-mcp-tool": "playcards_doppelkopf_score",
        "x-owner-app": "playcards",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaycardsDoppelkopfScoreInput"
              },
              "examples": {
                "example1": {
                  "summary": "Re wins with 150, Re announced, one fox caught",
                  "value": {
                    "reAugen": 150,
                    "reTricks": 8,
                    "kontraTricks": 4,
                    "reAnnounce": "1",
                    "reFox": 1
                  }
                },
                "example2": {
                  "summary": "Kontra wins against the Queens with 125",
                  "value": {
                    "reAugen": 115,
                    "reTricks": 5,
                    "kontraTricks": 7,
                    "kontraAnnounce": "1",
                    "kontraDoppelkopf": 1
                  }
                },
                "example3": {
                  "summary": "Re said keine 90 but got only 140",
                  "value": {
                    "reAugen": 140,
                    "reTricks": 7,
                    "kontraTricks": 5,
                    "reAnnounce": "2"
                  }
                },
                "example4": {
                  "summary": "Queens solo won keine 60",
                  "value": {
                    "solo": true,
                    "reAugen": 185,
                    "reTricks": 10,
                    "kontraTricks": 2,
                    "reAnnounce": "1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaycardsDoppelkopfScoreResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/playcards-blackjack-strategy/calculate": {
      "post": {
        "operationId": "calculatePlaycardsBlackjackStrategy",
        "summary": "Blackjack basic strategy chart and expected values computed for your table rules.",
        "description": "Computes the blackjack basic strategy for the rules at your table — number of decks, dealer hits or stands on soft 17, double after split, late surrender, which totals may double, 3:2 or 6:5 — from the exact dealer probabilities of the shoe, not from a copied chart. Enter a hand and the dealer’s up card to see the expected value of hit, stand, double, split and surrender. Play money and learning only: basic strategy lowers the house edge but never beats it.\n\nMethodology:\n- The dealer’s final totals (17–21 or bust) are computed exactly by recursion over the shoe with your two cards and the up card removed, conditioned on the dealer not having blackjack (US peek rule: with an Ace or ten-value up card the dealer checks first, so you only lose your original bet to a blackjack).\n- Hit, stand and double are valued with your extra cards drawn from the shoe with removal; a split hand is valued as a hand starting from one card (doubling only with double after split, split Aces get one card each, no re-splitting). Each hard total is averaged over its two-card combinations weighted by probability (total-dependent strategy).\n- The house edge sums every starting hand and up card with its probability, pays 3:2 or 6:5 for a natural, and assumes this strategy with no insurance. It is an estimate: re-splitting and composition-dependent plays are not modelled, so published figures can differ by about 0.05 %.\n\nFor learning and play money. No strategy removes the casino’s edge; gambling with real money can be addictive.",
        "tags": [
          "apps"
        ],
        "x-mcp-tool": "playcards_blackjack_strategy",
        "x-owner-app": "playcards",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaycardsBlackjackStrategyInput"
              },
              "examples": {
                "example1": {
                  "summary": "6 decks, dealer stands on soft 17, double after split",
                  "value": {
                    "decks": "6",
                    "h17": false,
                    "das": true
                  }
                },
                "example2": {
                  "summary": "Soft 18 against a 3 (6 decks, H17)",
                  "value": {
                    "decks": "6",
                    "h17": true,
                    "hand": "A,7",
                    "dealer": "3"
                  }
                },
                "example3": {
                  "summary": "Pair of eights against a 10, single deck",
                  "value": {
                    "decks": "1",
                    "h17": false,
                    "das": false,
                    "hand": "8,8",
                    "dealer": "10"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaycardsBlackjackStrategyResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/workflows": {
      "get": {
        "operationId": "listWorkflows",
        "summary": "Multi-tool workflows",
        "responses": {
          "200": {
            "description": "Workflow list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "PlaycardsSkatGameValueInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "game": {
            "title": "Game",
            "type": "string",
            "enum": [
              "D",
              "H",
              "S",
              "C",
              "grand",
              "null"
            ],
            "default": "C"
          },
          "matadorsWith": {
            "title": "Matadors",
            "type": "string",
            "enum": [
              "with",
              "without"
            ],
            "default": "with"
          },
          "matadors": {
            "title": "Number of matadors",
            "description": "Consecutive top trumps you hold (with) or miss (without), counting the Skat. Grand: at most 4.",
            "type": "integer",
            "minimum": 1,
            "maximum": 11,
            "default": 2
          },
          "hand": {
            "title": "Hand game (Skat not picked up)",
            "type": "boolean",
            "default": false
          },
          "schneider": {
            "title": "Schneider reached (90+ or 30 or less)",
            "type": "boolean",
            "default": false
          },
          "schneiderAnnounced": {
            "title": "Schneider announced (Hand only)",
            "type": "boolean",
            "default": false
          },
          "schwarz": {
            "title": "Schwarz reached (one side took every trick)",
            "type": "boolean",
            "default": false
          },
          "schwarzAnnounced": {
            "title": "Schwarz announced (Hand only)",
            "type": "boolean",
            "default": false
          },
          "ouvert": {
            "title": "Ouvert (cards open)",
            "type": "boolean",
            "default": false
          },
          "bid": {
            "title": "Your bid",
            "description": "0 = no bid check. Bids are game values from 18 upwards.",
            "type": "integer",
            "minimum": 0,
            "maximum": 264,
            "default": 0
          },
          "declarerPoints": {
            "title": "Declarer’s card points incl. Skat (optional)",
            "description": "With points the tool says whether the game is won and what it scores.",
            "type": "string",
            "maxLength": 3,
            "default": ""
          },
          "scoring": {
            "title": "Score sheet",
            "type": "string",
            "enum": [
              "isko",
              "seeger"
            ],
            "default": "isko"
          }
        },
        "required": []
      },
      "PlaycardsSkatGameValueResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "game",
              "baseValue",
              "multiplier",
              "value",
              "factors",
              "highestBidCovered",
              "bid",
              "overbid",
              "lostValue",
              "won",
              "score"
            ],
            "properties": {
              "game": {
                "type": "string"
              },
              "baseValue": {
                "type": "integer"
              },
              "multiplier": {
                "type": "integer"
              },
              "value": {
                "type": "integer"
              },
              "factors": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "label",
                    "count"
                  ],
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              },
              "highestBidCovered": {
                "type": "integer"
              },
              "bid": {
                "type": "integer"
              },
              "overbid": {
                "type": "boolean"
              },
              "lostValue": {
                "type": "integer",
                "description": "Value counted when the game is lost (at least the next multiple of the base value above an overbid)."
              },
              "won": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "score": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Score-sheet entry for the declarer (null without card points)."
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "PlaycardsDoppelkopfScoreInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "solo": {
            "title": "Solo (Re is one player)",
            "type": "boolean",
            "default": false
          },
          "reAugen": {
            "title": "Card points of the Re party",
            "description": "Kontra has 240 minus this.",
            "type": "integer",
            "minimum": 0,
            "maximum": 240,
            "default": 121
          },
          "reTricks": {
            "title": "Tricks won by Re",
            "type": "integer",
            "minimum": 0,
            "maximum": 12,
            "default": 6
          },
          "kontraTricks": {
            "title": "Tricks won by Kontra",
            "type": "integer",
            "minimum": 0,
            "maximum": 12,
            "default": 6
          },
          "reAnnounce": {
            "title": "Highest announcement of Re",
            "type": "string",
            "enum": [
              "0",
              "1",
              "2",
              "3",
              "4",
              "5"
            ],
            "default": "0"
          },
          "kontraAnnounce": {
            "title": "Highest announcement of Kontra",
            "type": "string",
            "enum": [
              "0",
              "1",
              "2",
              "3",
              "4",
              "5"
            ],
            "default": "0"
          },
          "reFox": {
            "title": "Foxes caught by Re",
            "type": "integer",
            "minimum": 0,
            "maximum": 2,
            "default": 0
          },
          "reKarlchen": {
            "title": "Karlchen for Re (last trick won with the Jack of Clubs)",
            "type": "integer",
            "minimum": 0,
            "maximum": 1,
            "default": 0
          },
          "reDoppelkopf": {
            "title": "Doppelköpfe won by Re (tricks with 40+ points)",
            "type": "integer",
            "minimum": 0,
            "maximum": 6,
            "default": 0
          },
          "kontraFox": {
            "title": "Foxes caught by Kontra",
            "type": "integer",
            "minimum": 0,
            "maximum": 2,
            "default": 0
          },
          "kontraKarlchen": {
            "title": "Karlchen for Kontra",
            "type": "integer",
            "minimum": 0,
            "maximum": 1,
            "default": 0
          },
          "kontraDoppelkopf": {
            "title": "Doppelköpfe won by Kontra",
            "type": "integer",
            "minimum": 0,
            "maximum": 6,
            "default": 0
          }
        },
        "required": []
      },
      "PlaycardsDoppelkopfScoreResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "winner",
              "reAugen",
              "kontraAugen",
              "lines",
              "value",
              "perPlayer"
            ],
            "properties": {
              "winner": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "re",
                  "kontra",
                  null
                ]
              },
              "reAugen": {
                "type": "integer"
              },
              "kontraAugen": {
                "type": "integer"
              },
              "lines": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "label",
                    "party",
                    "points"
                  ],
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "party": {
                      "type": "string"
                    },
                    "points": {
                      "type": "integer"
                    }
                  }
                }
              },
              "value": {
                "type": "integer",
                "description": "Points per Re player (negative when Re lost); Kontra players get the opposite."
              },
              "perPlayer": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "re",
                  "kontra"
                ],
                "properties": {
                  "re": {
                    "type": "integer"
                  },
                  "kontra": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "PlaycardsBlackjackStrategyInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "decks": {
            "title": "Decks",
            "type": "string",
            "enum": [
              "1",
              "2",
              "4",
              "6",
              "8"
            ],
            "default": "6"
          },
          "h17": {
            "title": "Dealer hits soft 17",
            "type": "boolean",
            "default": false
          },
          "das": {
            "title": "Double after split allowed",
            "type": "boolean",
            "default": true
          },
          "surrender": {
            "title": "Late surrender allowed",
            "type": "boolean",
            "default": false
          },
          "doubleOn": {
            "title": "Double on",
            "type": "string",
            "enum": [
              "any",
              "9-11",
              "10-11"
            ],
            "default": "any"
          },
          "bjPays": {
            "title": "Blackjack pays",
            "type": "string",
            "enum": [
              "3:2",
              "6:5"
            ],
            "default": "3:2"
          },
          "hand": {
            "title": "Your two cards (optional)",
            "description": "Ranks 2–10, J, Q, K, A separated by a comma.",
            "type": "string",
            "maxLength": 9,
            "default": ""
          },
          "dealer": {
            "title": "Dealer up card (optional)",
            "type": "string",
            "enum": [
              "",
              "2",
              "3",
              "4",
              "5",
              "6",
              "7",
              "8",
              "9",
              "10",
              "A"
            ],
            "default": ""
          }
        },
        "required": []
      },
      "PlaycardsBlackjackStrategyResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "rules",
              "houseEdgePercent",
              "hard",
              "soft",
              "pairs",
              "hand"
            ],
            "properties": {
              "rules": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "decks",
                  "h17",
                  "das",
                  "surrender",
                  "doubleOn",
                  "bjPays"
                ],
                "properties": {
                  "decks": {
                    "type": "integer"
                  },
                  "h17": {
                    "type": "boolean"
                  },
                  "das": {
                    "type": "boolean"
                  },
                  "surrender": {
                    "type": "boolean"
                  },
                  "doubleOn": {
                    "type": "string"
                  },
                  "bjPays": {
                    "type": "number"
                  }
                }
              },
              "houseEdgePercent": {
                "type": "number",
                "description": "Estimated house edge with this strategy (no re-splitting)."
              },
              "hard": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "soft": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "pairs": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "hand": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "required": [
                  "cards",
                  "dealer",
                  "best",
                  "evs"
                ],
                "properties": {
                  "cards": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "dealer": {
                    "type": "string"
                  },
                  "best": {
                    "type": "string"
                  },
                  "evs": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}