{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://marcallabs.co.uk/puzzle-schema.json",
  "title": "Castle Quest puzzle",
  "description": "One puzzle file for Castle Quest (Marcal Labs). Upload as puz/<id>.json. Rules this schema cannot express: (1) the characters < and > may not appear anywhere in the file, in keys or values; (2) every 'find' item must be different; (3) the item in a 'use' or 'give' step must be found by an earlier 'find' step; (4) each item can be used or given only once (it is used up); (5) in a 'riddle', 'correct' must be no bigger than the number of answers; (6) in a 'levers' step, 'order' must contain every name in 'levers' exactly once; (7) the file must be no bigger than 64 KB. Fields not listed here are ignored by the game. Check files at https://marcallabs.co.uk/puzzle-checker.html.",
  "type": "object",
  "required": ["id", "title", "steps"],
  "additionalProperties": false,
  "properties": {
    "id": { "type": "string", "pattern": "^[a-z0-9-]{1,40}$", "description": "Unique short name: small letters, numbers and dashes. Also the file name: <id>.json." },
    "title": { "type": "string", "minLength": 1, "maxLength": 60, "description": "Shown in the player's journal." },
    "places": {
      "type": "array", "minItems": 1, "uniqueItems": true,
      "items": { "$ref": "#/definitions/place" },
      "description": "Where the puzzle may appear. Default: [\"dungeon\", \"mine\", \"cave\", \"keep\"]. 'cave' means the small dens and the bigger cave networks; 'cellar' means the cellars under village inns."
    },
    "minLevel": { "type": "integer", "minimum": 1, "maximum": 20, "description": "Only used for players at this level or higher. Default 1." },
    "intro": { "type": "string", "minLength": 1, "maxLength": 400, "description": "Shown when the player first enters the place." },
    "things": {
      "type": "object",
      "description": "Optional descriptions, in your own words, of the things used in this puzzle (shown when the player walks up to them). Keys must be thing names.",
      "propertyNames": { "$ref": "#/definitions/thing" },
      "additionalProperties": { "type": "string", "minLength": 1, "maxLength": 200 }
    },
    "steps": { "type": "array", "minItems": 2, "maxItems": 8, "items": { "$ref": "#/definitions/step" }, "description": "Done strictly in order." },
    "reward": {
      "type": "object", "additionalProperties": false,
      "properties": {
        "gold": { "type": "integer", "minimum": 0, "maximum": 500, "description": "Default 0." },
        "xp": { "type": "integer", "minimum": 0, "maximum": 300, "description": "Experience. Default 40 if left out." },
        "magic": { "type": "boolean", "description": "true: a magic prize as in a vault (a better weapon, a magic item, an elixir of strength, or two healing potions). Default false." },
        "text": { "type": "string", "minLength": 1, "maxLength": 400, "description": "Shown when the puzzle is solved. Default: 'You have solved \"<title>\"!'" }
      }
    }
  },
  "definitions": {
    "place": { "enum": ["dungeon", "mine", "cave", "keep", "cellar"] },
    "thing": { "enum": ["bell", "statue", "altar", "carving", "door", "levers", "brazier", "well", "chest", "skeleton", "ghost", "prisoner"] },
    "monster": { "enum": ["rat", "goblin", "wolf", "spider", "bandit", "scorpion", "hag", "skeleton", "orc", "werewolf", "troll", "ogre", "giant", "bear", "dwarf"] },
    "text": { "type": "string", "minLength": 1, "maxLength": 400 },
    "item": { "type": "string", "minLength": 1, "maxLength": 40 },
    "step": {
      "type": "object",
      "required": ["type"],
      "properties": { "type": { "enum": ["find", "read", "use", "riddle", "levers", "fight", "give"] } },
      "allOf": [
        { "if": { "properties": { "type": { "const": "find" } } }, "then": {
          "description": "An item lying in its own spot somewhere in the place. Picking it up shows 'text'. It counts as done when its turn comes, even if picked up early.",
          "required": ["type", "item", "text"], "additionalProperties": false,
          "properties": { "type": {}, "item": { "$ref": "#/definitions/item" }, "text": { "$ref": "#/definitions/text" },
            "guard": { "$ref": "#/definitions/monster", "description": "A monster sitting on the item: it must be beaten before the item can be taken." } } } },
        { "if": { "properties": { "type": { "const": "read" } } }, "then": {
          "description": "Words to read. Without 'at', they are written on a wall in a spot of their own; with 'at', they are shown when the player walks up to that thing. Counts as done when its turn comes, even if read early.",
          "required": ["type", "text"], "additionalProperties": false,
          "properties": { "type": {}, "text": { "$ref": "#/definitions/text" }, "at": { "$ref": "#/definitions/thing" } } } },
        { "if": { "properties": { "type": { "const": "use" } } }, "then": {
          "description": "Use an item found earlier on a thing (the item is used up). Before its turn, trying it gives 'Nothing happens... yet'.",
          "required": ["type", "item", "at", "text"], "additionalProperties": false,
          "properties": { "type": {}, "item": { "$ref": "#/definitions/item" }, "at": { "$ref": "#/definitions/thing" }, "text": { "$ref": "#/definitions/text" } } } },
        { "if": { "properties": { "type": { "const": "give" } } }, "then": {
          "description": "Give an item found earlier to a thing, usually a ghost or a prisoner (the item is used up).",
          "required": ["type", "item", "at", "text"], "additionalProperties": false,
          "properties": { "type": {}, "item": { "$ref": "#/definitions/item" }, "at": { "$ref": "#/definitions/thing" }, "text": { "$ref": "#/definitions/text" } } } },
        { "if": { "properties": { "type": { "const": "riddle" } } }, "then": {
          "description": "A question asked at a thing, offered only when it is this step's turn. A wrong answer costs the player 6 strength and shows 'wrong'; they can try again.",
          "required": ["type", "at", "question", "answers", "correct"], "additionalProperties": false,
          "properties": { "type": {}, "at": { "$ref": "#/definitions/thing" },
            "question": { "type": "string", "minLength": 1, "maxLength": 300 },
            "answers": { "type": "array", "minItems": 2, "maxItems": 5, "items": { "type": "string", "minLength": 1, "maxLength": 60 } },
            "correct": { "type": "integer", "minimum": 1, "maximum": 5, "description": "Which answer is right, counting from 1." },
            "wrong": { "type": "string", "minLength": 1, "maxLength": 300 },
            "text": { "$ref": "#/definitions/text", "description": "Shown for the right answer. Optional: default 'That is the right answer!'" } } } },
        { "if": { "properties": { "type": { "const": "levers" } } }, "then": {
          "description": "Levers to pull in a set order at a thing (normally 'levers'), offered only when it is this step's turn. A wrong pull springs them all back, costs 4 strength and shows 'wrong'.",
          "required": ["type", "at", "levers", "order", "text"], "additionalProperties": false,
          "properties": { "type": {}, "at": { "$ref": "#/definitions/thing" },
            "levers": { "type": "array", "minItems": 2, "maxItems": 5, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 30 }, "description": "The levers' names, as the player sees them: 'Pull the <name> lever'." },
            "order": { "type": "array", "minItems": 2, "maxItems": 5, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 30 }, "description": "Every lever name once, in the order that works." },
            "wrong": { "type": "string", "minLength": 1, "maxLength": 300 }, "text": { "$ref": "#/definitions/text" } } } },
        { "if": { "properties": { "type": { "const": "fight" } } }, "then": {
          "description": "A guardian waiting in a spot of its own. Beating it shows 'text'. Counts as done when its turn comes, even if beaten early.",
          "required": ["type", "monster", "text"], "additionalProperties": false,
          "properties": { "type": {}, "monster": { "$ref": "#/definitions/monster" },
            "name": { "type": "string", "minLength": 1, "maxLength": 40, "description": "A name for the guardian, e.g. 'the Ash Warden'." },
            "text": { "$ref": "#/definitions/text" } } } }
      ]
    }
  }
}
