{
  "schemaVersion": 1,
  "slug": "bowling-score-calculator",
  "category": "sports",
  "kind": "implementation",
  "independentValidation": false,
  "implementationHash": "fd5eff7365ce3e5f4b7b623dad6e9c4a264839aa9a1ae89993b4a4cfd876731c",
  "provenance": "Generated from the calculation contract used by the published page. Publication is not external validation.",
  "sourceFiles": [
    "data/calculators/bowling-score-calculator.json",
    "src/engine.js"
  ],
  "runtimeSource": "/sources/engine.txt",
  "runtimeHash": "5c888d8898afbdee36267bb9977829e1e2ac568f6677c8ee87469c139864645c",
  "scope": "The compute function records implemented equations, constants, lookup tables and custom rules. Inputs record defaults and allowed values; outputs record precision and units. Defaults are examples, not recommendations. General background references are not evidence for this specific algorithm.",
  "publishedFormula": {
    "formula": "f_j = { 10+b₁+b₂, a_j=10; 10+b₁, a_j≠10 ∧ a_j+c_j=10; a_j+c_j, a_j+c_j<10 }; total = Σ_{j=1}^{10} f_j",
    "vars": [
      [
        "a_j",
        "First-ball pins in frame j"
      ],
      [
        "c_j",
        "Second-ball pins in frame j"
      ],
      [
        "b₁",
        "Pins on the next ball"
      ],
      [
        "b₂",
        "Pins two balls later"
      ],
      [
        "f_j",
        "Frame j score"
      ],
      [
        "total",
        "Total pinfall"
      ]
    ],
    "note": "Frames are walked in order: a strike consumes one ball and adds the next two balls, a spare consumes two and adds the next ball; iteration stops after 10 frames or when the input ends; handicap = max(0, round((basis−average)·0.8))."
  },
  "externalEvidence": null,
  "contract": {
    "compute": "const invalid={total:null,framesScored:null,strikes:null,spares:null,averagePerFrame:null,handicap:null,note:null,__error:'inputError'},raw=String(throws).trim(),tokens=raw?raw.split(/[\\s,]+/):[];if(!tokens.length||tokens.length>21||tokens.some(x=>!/^\\d+$/.test(x)||Number(x)>10))return invalid;const balls=tokens.map(Number);let i=0,total=0,framesScored=0,strikes=0,spares=0,tenthStart=-1;for(let f=0;f<10&&i<balls.length;f++){if(f===9)tenthStart=i;framesScored++;if(balls[i]===10){strikes++;const b1=balls[i+1],b2=balls[i+2];if(f===9&&b1!==undefined&&b2!==undefined&&b1<10&&b1+b2>10)return invalid;total+=10+(b1||0)+(b2||0);i++}else{const a=balls[i],b=balls[i+1]||0;if(a+b>10)return invalid;i+=2;if(a+b===10){spares++;total+=10+(balls[i]||0)}else total+=a+b}}const first=tenthStart>=0?balls[tenthStart]:undefined,second=tenthStart>=0?balls[tenthStart+1]:undefined,tenthComplete=framesScored===10&&(first===10?balls.length>=tenthStart+3:second!==undefined&&(first+second<10||balls.length>=tenthStart+3)),handicap=Math.max(0,Math.round((handicapBasis-average)*0.8)),note=!tenthComplete?'Partial game: '+framesScored+' frames entered; final or bonus rolls may be pending':'Strike and spare bonuses depend on following rolls';return{total,framesScored,strikes,spares,averagePerFrame:framesScored>0?total/framesScored:null,handicap,note};",
    "inputs": [
      {
        "name": "throws",
        "type": "text",
        "default": "10, 7, 3, 9, 0, 10, 10, 8, 2, 9, 1, 10, 10, 10",
        "step": 1
      },
      {
        "name": "average",
        "type": "number",
        "default": 165,
        "min": 0,
        "max": 300,
        "step": 1,
        "slider": true
      },
      {
        "name": "handicapBasis",
        "type": "number",
        "default": 200,
        "min": 150,
        "max": 240,
        "step": 5,
        "slider": true
      }
    ],
    "outputs": [
      {
        "name": "total",
        "precision": 0,
        "unit": ""
      },
      {
        "name": "framesScored",
        "precision": 0,
        "unit": ""
      },
      {
        "name": "strikes",
        "precision": 0,
        "unit": ""
      },
      {
        "name": "spares",
        "precision": 0,
        "unit": ""
      },
      {
        "name": "averagePerFrame",
        "precision": 1,
        "unit": ""
      },
      {
        "name": "handicap",
        "precision": 0,
        "unit": ""
      },
      {
        "name": "note",
        "precision": 0,
        "unit": "",
        "type": "text"
      }
    ],
    "interpret": null
  }
}
