{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "tabletwin/tactic.schema.json",
  "title": "Tabletwin tactic",
  "description": "A tactic: rules per decision place for a Tabletwin model. Rules are tried top to bottom; the first rule that decides picks. Expressions see the model's global names (zones, variables, derived values, constants, content, players, functions, modified()) plus opts, labels, where, me, pool, chosen, the names from let, and o and i in max_x, min_x and take_x. Engine locals such as actor and stack are not in scope. See docs/tactics.md.",
  "type": "object",
  "required": [
    "tactic"
  ],
  "additionalProperties": false,
  "properties": {
    "tactic": {
      "type": "string",
      "description": "Name of the tactic; agents show it as tactic:<name>."
    },
    "game": {
      "type": "string",
      "description": "The game id of the model this tactic is for; tactic.ts --check warns when it differs."
    },
    "label": {
      "type": "string",
      "description": "Display name, for people."
    },
    "summary": {
      "type": "string",
      "description": "What the tactic does, for people."
    },
    "note": {
      "type": "string"
    },
    "extends": {
      "type": "string",
      "description": "Path to a base tactic, relative to this file. This file's rules come before the base's rules; its let values replace the base's; every other key (label, default, search, ...) replaces the base's as a whole.",
      "examples": [
        "lotr-standard.yaml"
      ]
    },
    "let": {
      "type": "object",
      "additionalProperties": {
        "type": [
          "string",
          "number",
          "boolean"
        ]
      },
      "description": "Helper values {name: expression}, computed per decision and only when a rule uses them (also through another let).",
      "examples": [
        {
          "margin": "2",
          "questers": "[c['uid'] for c in pool if c.get('willpower', 0) > 1]"
        }
      ]
    },
    "default": {
      "enum": [
        "random",
        "first"
      ],
      "default": "random",
      "description": "What happens when no rule decides: a random option, or the first option."
    },
    "search": {
      "type": "object",
      "additionalProperties": false,
      "description": "Look-ahead for the search:<file>[:n] agent: at these places every other option is compared with the rules' pick by paired rollouts, and only replaces it when it is better by z standard errors.",
      "properties": {
        "at": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          ],
          "description": "Decision places to search (wildcards * ? [abc] allowed).",
          "examples": [
            [
              "action commit",
              "step defend"
            ]
          ]
        },
        "rollouts": {
          "type": "integer",
          "minimum": 1,
          "default": 8,
          "description": "Rollouts per option per round; the n in search:<file>:n overrides it."
        },
        "max_rollouts": {
          "type": "integer",
          "minimum": 1,
          "description": "At most this many rollouts per option (default 4 × rollouts)."
        },
        "z": {
          "type": "number",
          "minimum": 0,
          "default": 2,
          "description": "How many standard errors better an alternative must be to replace the rules' pick (default 2)."
        }
      }
    },
    "rules": {
      "type": "array",
      "description": "Rules, tried top to bottom. Each needs at and exactly one of pick, pick_x, max_x, min_x, take_x.",
      "items": {
        "type": "object",
        "required": [
          "at"
        ],
        "additionalProperties": false,
        "properties": {
          "at": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "description": "Decision place(s) as the engine names them: step <id>, action <name>, action <name> (cost), trigger <id>, window <name>, content <id>. Wildcards * ? [abc] allowed. A subset choice's suffix ' · n' is not part of the place.",
            "examples": [
              "step choice",
              "action attack",
              "trigger guard_dog",
              "window *",
              [
                "step plan",
                "action commit"
              ]
            ]
          },
          "options": {
            "type": "array",
            "description": "The rule only applies when the option labels are exactly this list, in order.",
            "examples": [
              [
                "use",
                "skip"
              ]
            ]
          },
          "when_x": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "The rule only applies when this expression is true.",
            "examples": [
              "pot >= 8 or score[me] + pot >= GOAL"
            ]
          },
          "pick": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Pick the option with this label. If no option has it, the rule does not decide.",
            "examples": [
              "stop",
              "pass",
              "use"
            ]
          },
          "pick_x": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Expression returning an option or a label; the option that is the same object or has the same label is picked. No match: the rule does not decide."
          },
          "max_x": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Score per option o (index i); the highest wins, the first on a tie. Tuples compare field by field.",
            "examples": [
              "(o['damage'], -o['cost'])"
            ]
          },
          "min_x": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Score per option o (index i); the lowest wins, the first on a tie."
          },
          "take_x": {
            "type": [
              "string",
              "number",
              "boolean"
            ],
            "description": "Subset choice: take candidate o? The first candidate for which it is true is picked; otherwise done (or, before the minimum is reached, the first candidate). pool holds all options, chosen what is already chosen.",
            "examples": [
              "o['uid'] in questers"
            ]
          },
          "note": {
            "type": "string"
          }
        },
        "oneOf": [
          {
            "required": [
              "pick"
            ]
          },
          {
            "required": [
              "pick_x"
            ]
          },
          {
            "required": [
              "max_x"
            ]
          },
          {
            "required": [
              "min_x"
            ]
          },
          {
            "required": [
              "take_x"
            ]
          }
        ]
      }
    }
  },
  "anyOf": [
    {
      "required": [
        "rules"
      ]
    },
    {
      "required": [
        "extends"
      ]
    }
  ]
}
