// src/playability.ts
var RIDDLE_PROOF_PLAYABILITY_VERSION = "riddle-proof.playability.v1";
var RIDDLE_PROOF_PLAYABILITY_ASSESSMENT_VERSION = "riddle-proof.playability.assessment.v1";
var PLAYABILITY_MODES = /* @__PURE__ */ new Set(["playable", "gameplay", "game"]);
var PLAYABILITY_CONTAINER_KEYS = [
  "playability",
  "playability_evidence",
  "playabilityEvidence",
  "playable",
  "gameplay",
  "gameplay_evidence",
  "gameplayEvidence"
];
var INPUT_ASSERTION_KEYS = [
  "inputAccepted",
  "inputObserved",
  "inputReceived",
  "controlsWorked",
  "keyboardWorked",
  "pointerWorked",
  "touchWorked",
  "steeringInputAccepted",
  "userInputObserved"
];
var STATE_ASSERTION_KEYS = [
  "stateChanged",
  "gameStateChanged",
  "playStarted",
  "simulationAdvanced",
  "distanceAdvanced",
  "scoreChanged",
  "hudChanged",
  "positionChanged",
  "speedChanged"
];
var MOTION_ASSERTION_KEYS = [
  "motionObserved",
  "visualMotion",
  "canvasChanged",
  "pixelChanged",
  "playfieldMoved",
  "playfieldPixelsChanged",
  "nonHudPixelsChanged",
  "animationAdvanced",
  "frameChanged",
  "framesChanged"
];
var TIME_ASSERTION_KEYS = [
  "timeProgressed",
  "clockAdvanced",
  "animationAdvanced",
  "simulationAdvanced",
  "playStarted"
];
var PERCENT_KEYS = [
  "changed_percent",
  "change_percent",
  "percent_changed",
  "diff_percent",
  "motion_percent",
  "pixel_change_percent"
];
var RATIO_KEYS = [
  "changed_ratio",
  "change_ratio",
  "diff_ratio",
  "motion_ratio",
  "pixel_change_ratio"
];
var PIXEL_KEYS = [
  "changed_pixels",
  "changed_pixel_count",
  "diff_pixels",
  "motion_pixels",
  "pixel_delta",
  "changedPixels"
];
var AVERAGE_DELTA_KEYS = [
  "average_delta",
  "avg_delta",
  "mean_delta",
  "avg_abs_delta",
  "mean_abs_delta"
];
var TIME_DELTA_KEYS = [
  "time_delta_ms",
  "elapsed_ms",
  "duration_ms",
  "sample_duration_ms",
  "animation_delta_ms"
];
function isRiddleProofPlayabilityMode(mode) {
  return PLAYABILITY_MODES.has(stringValue(mode).toLowerCase());
}
function extractPlayabilityEvidence(...sources) {
  const seen = /* @__PURE__ */ new Set();
  for (const source of sources) {
    const found = findPlayabilityEvidence(source, seen);
    if (found) return found;
  }
  return null;
}
function assessPlayabilityEvidence(evidence, options = {}) {
  const thresholds = {
    minChangedPercent: options.minChangedPercent ?? 0.5,
    minChangedPixels: options.minChangedPixels ?? 1e3,
    minAverageDelta: options.minAverageDelta ?? 1,
    minTimeDeltaMs: options.minTimeDeltaMs ?? 250
  };
  const required = {
    requireInput: options.requireInput ?? true,
    requireStateChange: options.requireStateChange ?? true,
    requireMotion: options.requireMotion ?? true,
    requireTimeProgression: options.requireTimeProgression ?? true
  };
  const record = extractPlayabilityEvidence(evidence);
  const metrics = {};
  const concerns = [];
  if (!record) {
    return {
      version: RIDDLE_PROOF_PLAYABILITY_ASSESSMENT_VERSION,
      evidence_present: false,
      passed: false,
      input_observed: false,
      state_changed: false,
      motion_observed: false,
      time_progressed: false,
      concerns: ["playability evidence is missing"],
      metrics,
      thresholds,
      required,
      evidence_keys: []
    };
  }
  const assertions = recordValue(record.assertions) || record;
  const inputObserved = inputObservedFrom(record, assertions);
  const stateChanged = stateChangedFrom(record, assertions, metrics);
  const motionObserved = motionObservedFrom(record, assertions, thresholds, metrics);
  const timeProgressed = timeProgressedFrom(record, assertions, thresholds, metrics);
  const explicitFailure = hasExplicitPlayabilityFailure(record, assertions);
  if (required.requireInput && !inputObserved) {
    concerns.push("no accepted player input was observed");
  }
  if (required.requireStateChange && !stateChanged) {
    concerns.push("game state did not measurably change");
  }
  if (required.requireMotion && !motionObserved) {
    concerns.push("playfield/canvas pixels did not measurably change");
  }
  if (required.requireTimeProgression && !timeProgressed) {
    concerns.push("play time or animation time did not measurably progress");
  }
  if (explicitFailure) {
    concerns.push("playability evidence includes an explicit failed assertion");
  }
  const passed = Boolean(
    (!required.requireInput || inputObserved) && (!required.requireStateChange || stateChanged) && (!required.requireMotion || motionObserved) && (!required.requireTimeProgression || timeProgressed) && !explicitFailure
  );
  return {
    version: RIDDLE_PROOF_PLAYABILITY_ASSESSMENT_VERSION,
    evidence_present: true,
    passed,
    input_observed: inputObserved,
    state_changed: stateChanged,
    motion_observed: motionObserved,
    time_progressed: timeProgressed,
    concerns,
    metrics,
    thresholds,
    required,
    evidence_keys: Object.keys(record)
  };
}
function findPlayabilityEvidence(value, seen, depth = 0) {
  if (depth > 6 || value === null || value === void 0) return null;
  if (typeof value === "string") {
    const parsed = parseJson(value);
    return parsed === null ? null : findPlayabilityEvidence(parsed, seen, depth + 1);
  }
  if (Array.isArray(value)) {
    if (seen.has(value)) return null;
    seen.add(value);
    for (const item of value) {
      const found = findPlayabilityEvidence(item, seen, depth + 1);
      if (found) return found;
    }
    return null;
  }
  const record = recordValue(value);
  if (!record || seen.has(record)) return null;
  seen.add(record);
  if (record.version === RIDDLE_PROOF_PLAYABILITY_VERSION || hasPlayabilityShape(record)) {
    return record;
  }
  for (const key of PLAYABILITY_CONTAINER_KEYS) {
    if (Object.prototype.hasOwnProperty.call(record, key)) {
      const nested = findPlayabilityEvidence(record[key], seen, depth + 1);
      if (nested) return nested;
      if (typeof record[key] === "boolean") {
        return { assertions: { [key]: record[key] } };
      }
    }
  }
  for (const item of Object.values(record)) {
    const found = findPlayabilityEvidence(item, seen, depth + 1);
    if (found) return found;
  }
  return null;
}
function hasPlayabilityShape(record) {
  return Boolean(
    record.input_events || record.state_delta || record.pixel_delta || record.canvas_delta || record.motion_delta || record.playfield_delta || record.time_delta_ms !== void 0 || hasAnyKey(recordValue(record.assertions) || record, [
      ...INPUT_ASSERTION_KEYS,
      ...STATE_ASSERTION_KEYS,
      ...MOTION_ASSERTION_KEYS,
      ...TIME_ASSERTION_KEYS
    ])
  );
}
function inputObservedFrom(record, assertions) {
  if (trueForAnyKey(assertions, INPUT_ASSERTION_KEYS)) return true;
  if (listValue(record.input_events).length > 0) return true;
  if (listValue(record.inputs).length > 0) return true;
  if (listValue(record.interactions).length > 0) return true;
  const eventCount = numericFromAnyKey(record, ["input_event_count", "input_count", "interaction_count"]);
  return eventCount !== null && eventCount > 0;
}
function stateChangedFrom(record, assertions, metrics) {
  if (trueForAnyKey(assertions, STATE_ASSERTION_KEYS)) return true;
  const stateDelta = recordValue(record.state_delta) || recordValue(record.stateDelta);
  if (stateDelta) {
    const changedKeys = listValue(stateDelta.changed_keys || stateDelta.changedKeys);
    metrics.state_changed_keys = changedKeys;
    if (stateDelta.changed === true || changedKeys.length > 0) return true;
  }
  const delta = numericFromAnyKey(record, [
    "distance_delta",
    "score_delta",
    "position_delta",
    "speed_delta",
    "hud_delta"
  ]);
  if (delta !== null) {
    metrics.state_numeric_delta = delta;
    return Math.abs(delta) > 0;
  }
  return false;
}
function motionObservedFrom(record, assertions, thresholds, metrics) {
  if (trueForAnyKey(assertions, MOTION_ASSERTION_KEYS)) return true;
  for (const source of [
    recordValue(record.playfield_delta),
    recordValue(record.playfieldDelta),
    recordValue(record.non_hud_delta),
    recordValue(record.nonHudDelta),
    recordValue(record.pixel_delta),
    recordValue(record.pixelDelta),
    recordValue(record.canvas_delta),
    recordValue(record.canvasDelta),
    recordValue(record.motion_delta),
    recordValue(record.motionDelta),
    recordValue(record.visual_delta),
    recordValue(record.visualDelta),
    record
  ]) {
    if (!source) continue;
    const percent = percentFrom(source);
    const pixels = numericFromAnyKey(source, PIXEL_KEYS);
    const averageDelta = numericFromAnyKey(source, AVERAGE_DELTA_KEYS);
    if (percent !== null) metrics.changed_percent = percent;
    if (pixels !== null) metrics.changed_pixels = pixels;
    if (averageDelta !== null) metrics.average_delta = averageDelta;
    if (percent !== null && percent >= thresholds.minChangedPercent) return true;
    if (pixels !== null && pixels >= thresholds.minChangedPixels) return true;
    if (averageDelta !== null && averageDelta >= thresholds.minAverageDelta) return true;
  }
  return false;
}
function timeProgressedFrom(record, assertions, thresholds, metrics) {
  if (trueForAnyKey(assertions, TIME_ASSERTION_KEYS)) return true;
  const timeDelta = numericFromAnyKey(record, TIME_DELTA_KEYS);
  if (timeDelta !== null) {
    metrics.time_delta_ms = timeDelta;
    return timeDelta >= thresholds.minTimeDeltaMs;
  }
  const stateDelta = recordValue(record.state_delta) || recordValue(record.stateDelta);
  const nestedDelta = numericFromAnyKey(stateDelta || {}, TIME_DELTA_KEYS);
  if (nestedDelta !== null) {
    metrics.time_delta_ms = nestedDelta;
    return nestedDelta >= thresholds.minTimeDeltaMs;
  }
  return false;
}
function hasExplicitPlayabilityFailure(record, assertions) {
  if (record.passed === false || record.playable === false || assertions.playabilityPassed === false) {
    return true;
  }
  for (const key of [
    ...INPUT_ASSERTION_KEYS,
    ...STATE_ASSERTION_KEYS,
    ...MOTION_ASSERTION_KEYS,
    ...TIME_ASSERTION_KEYS
  ]) {
    if (assertions[key] === false) return true;
  }
  return false;
}
function trueForAnyKey(record, keys) {
  return keys.some((key) => record[key] === true);
}
function hasAnyKey(record, keys) {
  return keys.some((key) => Object.prototype.hasOwnProperty.call(record, key));
}
function percentFrom(record) {
  const percent = numericFromAnyKey(record, PERCENT_KEYS);
  if (percent !== null) return percent;
  const ratio = numericFromAnyKey(record, RATIO_KEYS);
  if (ratio !== null) return ratio <= 1 ? ratio * 100 : ratio;
  return null;
}
function numericFromAnyKey(record, keys) {
  for (const key of keys) {
    const value = numericValue(record[key]);
    if (value !== null) return value;
  }
  return null;
}
function recordValue(value) {
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function listValue(value) {
  return Array.isArray(value) ? value : [];
}
function numericValue(value) {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string" && value.trim()) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}
function stringValue(value) {
  return typeof value === "string" && value.trim() ? value.trim() : "";
}
function parseJson(value) {
  const trimmed = value.trim();
  if (!trimmed || !/^[{[]/.test(trimmed)) return null;
  try {
    return JSON.parse(trimmed);
  } catch {
    return null;
  }
}

export {
  RIDDLE_PROOF_PLAYABILITY_VERSION,
  RIDDLE_PROOF_PLAYABILITY_ASSESSMENT_VERSION,
  isRiddleProofPlayabilityMode,
  extractPlayabilityEvidence,
  assessPlayabilityEvidence
};
