Drop7 Research
approaches/oracle-curriculum/topology/oracle-topology-audit.cppC++1,410 lines · 55.6 KB
#include "../../../src/core/native/public-behavior.hpp"

#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <compare>
#include <cstdint>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <mutex>
#include <numeric>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>

// Analysis-only comparison between a public-state exact expectimax policy and
// a deliberately privileged oracle which sees the realized headless random
// tape.  Oracle information must never cross into a deployed policy.
namespace drop7::oracle_topology {

using Clock = std::chrono::steady_clock;

constexpr std::uint32_t kDefaultSeedStart = 0x3d70'f000u;
constexpr std::uint32_t kTrainingStart = 0x3d00'0000u;
constexpr std::uint32_t kTrainingEnd = 0x3e00'0000u;
constexpr int kDefaultGames = 16;
constexpr int kDefaultMaximumMoves = 200;
constexpr int kDefaultOracleDepth = 4;
constexpr int kDefaultOracleBeam = 128;
constexpr int kMoveBandWidth = 20;
constexpr int kOccupancyBinWidth = 4;
constexpr int kHeightBinWidth = 2;
constexpr double kMinimumStableEffect = 0.10;
constexpr int kMinimumSeedsPerHalf = 6;
constexpr double kTerminalPenalty = -1'000'000'000.0;

std::mutex progress_mutex;

struct Config {
  std::uint32_t seed_start = kDefaultSeedStart;
  int games = kDefaultGames;
  int maximum_moves = kDefaultMaximumMoves;
  int oracle_depth = kDefaultOracleDepth;
  int oracle_beam = kDefaultOracleBeam;
  int parallelism = 4;
  std::string json = "/tmp/drop7-oracle-topology-audit.json";
  std::string readme = "/tmp/drop7-oracle-topology-audit-README.md";
};

struct OracleStats {
  std::uint64_t generated = 0;
  std::uint64_t deduplicated = 0;
  std::size_t peak_candidates = 0;
};

struct OraclePlan {
  int column = -1;
  OracleStats stats{};
};

struct BeamNode {
  State state{};
  int first_column = -1;
  std::string dynamic_key;
  double rank = -std::numeric_limits<double>::infinity();
};

double originalCombinedUtility(const State& state) {
  if (state.game_over) return -250'000.0;
  const cfpi::detail::PhaseFeatures f =
      cfpi::detail::extractPhaseFeatures(state);
  return 180.0 * f.open_columns - 10.0 * f.height_load -
         620.0 * f.solid_cells - 220.0 * f.cracked_cells -
         18.0 * f.numbered_cells - 90.0 * f.high_low_numbers +
         140.0 * f.direct_potential + 360.0 * f.latent_chain_potential +
         100.0 * f.cracked_exposure + 40.0 * f.solid_exposure -
         550.0 * f.adjacent_ones - 750.0 * f.triple_twos -
         120.0 * f.dead_low_numbers;
}

double rankState(const State& state) {
  return static_cast<double>(state.score) + originalCombinedUtility(state) +
         (state.game_over ? kTerminalPenalty : 0.0);
}

std::string oracleDynamicKey(const State& state) {
  std::string key = serializeBoard(state.board);
  key.push_back('|');
  key += std::to_string(state.next_disc);
  key.push_back('|');
  key += std::to_string(state.level);
  key.push_back('|');
  key += std::to_string(state.moves_remaining);
  key.push_back('|');
  key += std::to_string(state.moves_played);
  key.push_back('|');
  key.push_back(state.game_over ? '1' : '0');
  return key;
}

bool betterBeamNode(const BeamNode& left, const BeamNode& right) {
  if (left.rank != right.rank) return left.rank > right.rank;
  if (left.state.score != right.state.score) {
    return left.state.score > right.state.score;
  }
  if (left.first_column != right.first_column) {
    return left.first_column < right.first_column;
  }
  return left.dynamic_key < right.dynamic_key;
}

void insertCandidate(std::unordered_map<std::string, BeamNode>& candidates,
                     BeamNode candidate, OracleStats& stats) {
  const auto found = candidates.find(candidate.dynamic_key);
  if (found == candidates.end()) {
    candidates.emplace(candidate.dynamic_key, std::move(candidate));
    return;
  }
  ++stats.deduplicated;
  if (candidate.state.score > found->second.state.score ||
      (candidate.state.score == found->second.state.score &&
       candidate.first_column < found->second.first_column)) {
    found->second = std::move(candidate);
  }
}

OraclePlan planOracleMove(const State& root, std::uint32_t game_seed,
                          int depth, int beam_width) {
  if (root.game_over) return {};
  if (depth < 1 || depth > 12 || beam_width < 1 || beam_width > 2'048) {
    throw std::invalid_argument("oracle work bounds are invalid");
  }
  std::vector<BeamNode> beam{{root, -1, oracleDynamicKey(root),
                              rankState(root)}};
  OracleStats stats;
  for (int ply = 0; ply < depth; ++ply) {
    std::unordered_map<std::string, BeamNode> candidates;
    candidates.reserve(static_cast<std::size_t>(beam_width * kBoardSize));
    for (const BeamNode& node : beam) {
      if (node.state.game_over) {
        insertCandidate(candidates, node, stats);
        continue;
      }
      int legal_count = 0;
      const auto legal = legalColumns(node.state.board, legal_count);
      for (int offset = 0; offset < legal_count; ++offset) {
        const int column = legal[offset];
        State next = node.state;
        MoveResult move;
        if (!playHeadlessMove(next, game_seed, column, move)) continue;
        ++stats.generated;
        BeamNode candidate;
        candidate.state = std::move(next);
        candidate.first_column =
            node.first_column < 0 ? column : node.first_column;
        candidate.dynamic_key = oracleDynamicKey(candidate.state);
        insertCandidate(candidates, std::move(candidate), stats);
      }
    }
    if (candidates.empty()) break;
    stats.peak_candidates =
        std::max(stats.peak_candidates, candidates.size());
    std::vector<BeamNode> ranked;
    ranked.reserve(candidates.size());
    for (auto& entry : candidates) {
      entry.second.rank = rankState(entry.second.state);
      ranked.push_back(std::move(entry.second));
    }
    std::sort(ranked.begin(), ranked.end(), betterBeamNode);
    if (static_cast<int>(ranked.size()) > beam_width) {
      ranked.resize(static_cast<std::size_t>(beam_width));
    }
    beam = std::move(ranked);
  }
  std::sort(beam.begin(), beam.end(), betterBeamNode);
  for (const BeamNode& node : beam) {
    if (node.first_column >= 0) return {node.first_column, stats};
  }
  return {-1, stats};
}

enum Feature : std::size_t {
  kOccupied,
  kMaximumHeight,
  kHeightLoad,
  kSolidCells,
  kCrackedCells,
  kNumberedCells,
  kDirectPotential,
  kLatentChainPotential,
  kTriggerReadiness,
  kRiseTriggerReadiness,
  kQuietBuildOptions,
  kQuietDirectGain,
  kCrackedExposure,
  kSolidExposure,
  kCoverAltitudeDebt,
  kSolidAltitude,
  kCrackedAltitude,
  kCoveredCliffAccess,
  kEdgeCliffAccess,
  kHighNumberColumnCohesion,
  kHighNumberCliffCohesion,
  kStoredHighNumbers,
  kAdjacentOnes,
  kTripleTwos,
  kDeadLowNumbers,
  kLowCapLoad,
  kAdjacentLowCapLoad,
  kProjectedOccupancyDebt,
  kPeakHeightRisk,
  kNumberedCleared,
  kCoversRevealed,
  kChainWaves,
  kMaximumChainDepth,
  kMultiwaveMove,
  kCascadePoints,
  kBoardClear,
  kFeatureCount,
};

using Features = std::array<double, kFeatureCount>;

struct FeatureSpec {
  const char* name;
  const char* category;
  const char* kind;
  const char* interpretation;
};

constexpr std::array<FeatureSpec, kFeatureCount> kFeatureSpecs{{
    {"occupied", "balance", "state", "occupied board cells"},
    {"maximumHeight", "balance", "state", "highest column"},
    {"heightLoad", "coverAltitudeDamage", "state", "squared altitude load"},
    {"solidCells", "coverAltitudeDamage", "state", "undamaged covers"},
    {"crackedCells", "coverAltitudeDamage", "state", "partially damaged covers"},
    {"numberedCells", "balance", "state", "visible numbered discs"},
    {"directPotential", "storedEnergy", "state", "direct trigger readiness"},
    {"latentChainPotential", "storedEnergy", "state", "release readiness behind another trigger"},
    {"triggerReadiness", "storedEnergy", "state", "immediate legal-drop trigger inventory"},
    {"riseTriggerReadiness", "storedEnergy", "state", "stored trigger after the next rise"},
    {"quietBuildOptions", "storedEnergy", "state", "legal non-triggering build choices"},
    {"quietDirectGain", "storedEnergy", "state", "best quiet increase in direct readiness"},
    {"crackedExposure", "coverAccess", "state", "reachable cracked covers"},
    {"solidExposure", "coverAccess", "state", "two-hit solid-cover exposure"},
    {"coverAltitudeDebt", "coverAltitudeDamage", "state", "altitude-weighted cover burden"},
    {"solidAltitude", "coverAltitudeDamage", "state", "squared altitude of solid covers"},
    {"crackedAltitude", "coverAltitudeDamage", "state", "squared altitude of cracked covers"},
    {"coveredCliffAccess", "coverAccess", "state", "covered cliff faces reachable from a low channel"},
    {"edgeCliffAccess", "coverAccess", "state", "reachable covered cliff faces on board edges"},
    {"highNumberColumnCohesion", "highNumberTrench", "state", "repeated live 5/6/7 discs sharing a vertical trigger"},
    {"highNumberCliffCohesion", "highNumberTrench", "state", "high-number cohesion adjacent to covered cliffs"},
    {"storedHighNumbers", "highNumberTrench", "state", "unfired high-number readiness"},
    {"adjacentOnes", "lowNumberClog", "state", "paired ones with no easy escape"},
    {"tripleTwos", "lowNumberClog", "state", "runs of three or more twos"},
    {"deadLowNumbers", "lowNumberClog", "state", "oversized low discs with weak release paths"},
    {"lowCapLoad", "lowNumberClog", "state", "height-weighted columns capped by one or two"},
    {"adjacentLowCapLoad", "lowNumberClog", "state", "neighboring low-number column caps"},
    {"projectedOccupancyDebt", "balance", "state", "load projected through the next rise"},
    {"peakHeightRisk", "balance", "state", "phase-adjusted cubic height risk"},
    {"numberedCleared", "clearRevealThroughput", "transitionOutcome", "numbered discs cleared on the following move"},
    {"coversRevealed", "clearRevealThroughput", "transitionOutcome", "covers revealed on the following move"},
    {"chainWaves", "clearRevealThroughput", "transitionOutcome", "cascade waves on the following move"},
    {"maximumChainDepth", "clearRevealThroughput", "transitionOutcome", "deepest following cascade wave"},
    {"multiwaveMove", "clearRevealThroughput", "transitionOutcome", "following move has multiple waves"},
    {"cascadePoints", "clearRevealThroughput", "transitionOutcome", "following cascade score excluding bonuses"},
    {"boardClear", "clearRevealThroughput", "transitionOutcome", "following move clears the board"},
}};

double readiness(int required) {
  return required >= 1 ? std::ldexp(1.0, 1 - required) : 0.0;
}

double unionReadiness(double first, double second) {
  return 1.0 - (1.0 - first) * (1.0 - second);
}

std::array<int, kBoardSize> heights(const Board& board) {
  std::array<int, kBoardSize> result{};
  for (int column = 0; column < kBoardSize; ++column) {
    for (int row = 0; row < kBoardSize; ++row) {
      result[column] += board[indexOf(row, column)] != kEmpty;
    }
  }
  return result;
}

int adjacentCoveredCliffDepth(
    const Board& board, int column, int height,
    const std::array<int, kBoardSize>& column_heights) {
  int result = 0;
  for (const int neighbor : {column - 1, column + 1}) {
    if (neighbor < 0 || neighbor >= kBoardSize) continue;
    for (int elevation = height + 1;
         elevation <= column_heights[neighbor]; ++elevation) {
      const std::uint8_t cell =
          board[indexOf(kBoardSize - elevation, neighbor)];
      result += cell == kSolid || cell == kCracked;
    }
  }
  return result;
}

Features extractFeatures(const State& state, const MoveResult& move) {
  Features result{};
  const cfpi::detail::PhaseFeatures phase =
      cfpi::detail::extractPhaseFeatures(state);
  const auto column_heights = heights(state.board);
  int occupied = 0;
  int maximum_height = 0;
  for (const int height : column_heights) maximum_height = std::max(maximum_height, height);
  for (const std::uint8_t cell : state.board) occupied += cell != kEmpty;
  result[kOccupied] = occupied;
  result[kMaximumHeight] = maximum_height;
  result[kHeightLoad] = phase.height_load;
  result[kSolidCells] = phase.solid_cells;
  result[kCrackedCells] = phase.cracked_cells;
  result[kNumberedCells] = phase.numbered_cells;
  result[kDirectPotential] = phase.direct_potential;
  result[kLatentChainPotential] = phase.latent_chain_potential;
  result[kTriggerReadiness] = phase.trigger_readiness;
  result[kRiseTriggerReadiness] = phase.rise_trigger_readiness;
  result[kQuietBuildOptions] = phase.quiet_build_options;
  result[kQuietDirectGain] = phase.quiet_direct_gain;
  result[kCrackedExposure] = phase.cracked_exposure;
  result[kSolidExposure] = phase.solid_exposure;
  result[kCoverAltitudeDebt] = phase.cover_altitude_debt;
  result[kAdjacentOnes] = phase.adjacent_ones;
  result[kTripleTwos] = phase.triple_twos;
  result[kDeadLowNumbers] = phase.dead_low_numbers;
  result[kLowCapLoad] = phase.low_cap_load;
  result[kAdjacentLowCapLoad] = phase.adjacent_low_cap_load;
  result[kProjectedOccupancyDebt] = phase.projected_occupancy_debt;
  result[kPeakHeightRisk] = phase.peak_height_risk;

  for (int row = 0; row < kBoardSize; ++row) {
    const int elevation = kBoardSize - row;
    for (int column = 0; column < kBoardSize; ++column) {
      const std::uint8_t cell = state.board[indexOf(row, column)];
      if (cell != kSolid && cell != kCracked) continue;
      const double altitude = static_cast<double>(elevation * elevation);
      if (cell == kSolid) result[kSolidAltitude] += altitude;
      else result[kCrackedAltitude] += altitude;
      const double cover_factor = cell == kSolid ? 1.0 : 0.68;
      const auto access_from = [&](int neighbor) {
        if (neighbor < 0 || neighbor >= kBoardSize ||
            column_heights[neighbor] >= elevation) {
          return 0.0;
        }
        return readiness(elevation - column_heights[neighbor]);
      };
      const double access = unionReadiness(access_from(column - 1),
                                           access_from(column + 1)) *
                            altitude * cover_factor;
      result[kCoveredCliffAccess] += access;
      if (column == 0 || column == kBoardSize - 1) {
        result[kEdgeCliffAccess] += access;
      }
    }
  }

  for (int column = 0; column < kBoardSize; ++column) {
    const int height = column_heights[column];
    std::array<int, 3> high_counts{};
    for (int row = kBoardSize - height; row < kBoardSize; ++row) {
      if (row < 0) continue;
      const std::uint8_t cell = state.board[indexOf(row, column)];
      if (cell >= 5 && cell <= 7 && cell > height) {
        ++high_counts[cell - 5];
      }
      if (cell >= 5 && cell <= 7) {
        const int horizontal = lineLength(state.board, row, column, false);
        const int vertical = lineLength(state.board, row, column, true);
        double ready = 0.0;
        if (horizontal < cell) ready = readiness(cell - horizontal);
        if (vertical < cell) {
          ready = unionReadiness(ready, readiness(cell - vertical));
        }
        if (ready > 0 && horizontal != cell && vertical != cell) {
          result[kStoredHighNumbers] += ready * (cell - 3) / 4.0;
        }
      }
    }
    const int cliff_depth = adjacentCoveredCliffDepth(
        state.board, column, height, column_heights);
    for (int offset = 0; offset < 3; ++offset) {
      const int count = high_counts[offset];
      if (count < 2) continue;
      const int value = offset + 5;
      const double cohesion =
          (count * (count - 1) / 2.0) * readiness(value - height);
      result[kHighNumberColumnCohesion] += cohesion;
      result[kHighNumberCliffCohesion] +=
          cohesion * std::min(3, cliff_depth);
    }
  }

  std::int64_t cascade_points = 0;
  int maximum_chain_depth = 0;
  for (const Wave& wave : move.waves) {
    result[kNumberedCleared] += wave.cleared;
    result[kCoversRevealed] += wave.revealed;
    cascade_points += wave.points;
    maximum_chain_depth = std::max(maximum_chain_depth, wave.depth);
  }
  result[kChainWaves] = move.waves.size();
  result[kMaximumChainDepth] = maximum_chain_depth;
  result[kMultiwaveMove] = move.waves.size() > 1;
  result[kCascadePoints] = static_cast<double>(cascade_points);
  result[kBoardClear] = move.cleared_board;
  for (const double value : result) {
    if (!std::isfinite(value)) {
      throw std::runtime_error("topology feature is non-finite");
    }
  }
  return result;
}

enum class Policy { kExact, kOracle };

struct StateRecord {
  std::uint32_t seed = 0;
  Policy policy = Policy::kExact;
  int move_band = 0;
  int rise_phase = 0;
  int occupancy_bin = 0;
  int height_bin = 0;
  Features features{};
};

struct GameResult {
  std::uint32_t seed = 0;
  Policy policy = Policy::kExact;
  std::int64_t score = 0;
  int moves = 0;
  bool censored = false;
  std::uint64_t work = 0;
  std::uint64_t generated = 0;
  std::uint64_t deduplicated = 0;
  std::size_t peak_candidates = 0;
  std::uint64_t completed_depth_sum = 0;
  std::uint64_t completed_depth_three = 0;
  std::uint64_t cleared = 0;
  std::uint64_t revealed = 0;
  std::uint64_t waves = 0;
  int maximum_chain_depth = 0;
  double elapsed_seconds = 0.0;
  std::vector<StateRecord> records;
};

State publicState(const State& source) {
  State result;
  result.board = source.board;
  result.next_disc = source.next_disc;
  result.moves_remaining = source.moves_remaining;
  result.score = 0;
  result.level = 1;
  result.moves_played = 0;
  result.game_over = false;
  return result;
}

StateRecord makeRecord(std::uint32_t seed, Policy policy,
                       const State& state, const MoveResult& move) {
  const cfpi::PhaseMetrics metrics = cfpi::evaluatePhaseMetrics(state);
  StateRecord result;
  result.seed = seed;
  result.policy = policy;
  result.move_band = state.moves_played / kMoveBandWidth;
  result.rise_phase = state.moves_remaining;
  result.occupancy_bin = metrics.occupied / kOccupancyBinWidth;
  result.height_bin = metrics.maximum_height / kHeightBinWidth;
  result.features = extractFeatures(publicState(state), move);
  return result;
}

void addThroughput(GameResult& result, const MoveResult& move) {
  for (const Wave& wave : move.waves) {
    result.cleared += wave.cleared;
    result.revealed += wave.revealed;
    result.maximum_chain_depth =
        std::max(result.maximum_chain_depth, wave.depth);
  }
  result.waves += move.waves.size();
}

GameResult runExactGame(std::uint32_t seed, const Config& config) {
  const auto started = Clock::now();
  GameResult result;
  result.seed = seed;
  result.policy = Policy::kExact;
  result.records.reserve(config.maximum_moves);
  State state = initialHeadlessState(seed);
  cfpi::BehaviorOptions options;
  options.max_depth = 3;
  options.chance_samples = 5;
  options.max_work = 1'000'000;
  options.max_cache_entries = 40'000;
  options.terminal_utility = -1'000'000.0;
  while (!state.game_over && state.moves_played < config.maximum_moves) {
    cfpi::BehaviorMetrics metrics;
    const int action =
        cfpi::chooseBehaviorAction(publicState(state), options, &metrics);
    if (!isLegal(state.board, action)) {
      throw std::runtime_error("exact audit policy chose an illegal action");
    }
    const State before = state;
    MoveResult move;
    if (!playHeadlessMove(state, seed, action, move)) {
      throw std::runtime_error("exact audit transition failed");
    }
    result.records.push_back(makeRecord(seed, Policy::kExact, before, move));
    result.work += metrics.work;
    result.completed_depth_sum += metrics.completed_depth;
    result.completed_depth_three += metrics.completed_depth == 3;
    addThroughput(result, move);
  }
  result.score = state.score;
  result.moves = state.moves_played;
  result.censored = !state.game_over;
  result.elapsed_seconds =
      std::chrono::duration<double>(Clock::now() - started).count();
  return result;
}

GameResult runOracleGame(std::uint32_t seed, const Config& config) {
  const auto started = Clock::now();
  GameResult result;
  result.seed = seed;
  result.policy = Policy::kOracle;
  result.records.reserve(config.maximum_moves);
  State state = initialHeadlessState(seed);
  while (!state.game_over && state.moves_played < config.maximum_moves) {
    const OraclePlan plan = planOracleMove(
        state, seed, config.oracle_depth, config.oracle_beam);
    if (!isLegal(state.board, plan.column)) {
      throw std::runtime_error("privileged oracle chose an illegal action");
    }
    const State before = state;
    MoveResult move;
    if (!playHeadlessMove(state, seed, plan.column, move)) {
      throw std::runtime_error("oracle audit transition failed");
    }
    result.records.push_back(makeRecord(seed, Policy::kOracle, before, move));
    result.generated += plan.stats.generated;
    result.deduplicated += plan.stats.deduplicated;
    result.peak_candidates =
        std::max(result.peak_candidates, plan.stats.peak_candidates);
    addThroughput(result, move);
  }
  result.score = state.score;
  result.moves = state.moves_played;
  result.censored = !state.game_over;
  result.elapsed_seconds =
      std::chrono::duration<double>(Clock::now() - started).count();
  return result;
}

struct Cohort {
  std::vector<GameResult> exact;
  std::vector<GameResult> oracle;
  double wall_seconds = 0.0;
};

Cohort runCohort(const Config& config) {
  const auto started = Clock::now();
  Cohort result;
  result.exact.resize(config.games);
  result.oracle.resize(config.games);
  std::atomic<int> next_game{0};
  std::vector<std::future<void>> workers;
  const int worker_count = std::min(config.parallelism, config.games);
  for (int worker = 0; worker < worker_count; ++worker) {
    workers.push_back(std::async(std::launch::async, [&] {
      for (;;) {
        const int game = next_game.fetch_add(1);
        if (game >= config.games) return;
        const std::uint32_t seed =
            config.seed_start + static_cast<std::uint32_t>(game);
        result.exact[game] = runExactGame(seed, config);
        result.oracle[game] = runOracleGame(seed, config);
        const std::lock_guard<std::mutex> lock(progress_mutex);
        std::cerr << "oracle-topology " << game + 1 << '/' << config.games
                  << " seed 0x" << std::hex << seed << std::dec
                  << " exact " << result.exact[game].score << '/'
                  << result.exact[game].moves << " oracle "
                  << result.oracle[game].score << '/'
                  << result.oracle[game].moves << '\n';
      }
    }));
  }
  for (auto& worker : workers) worker.get();
  result.wall_seconds =
      std::chrono::duration<double>(Clock::now() - started).count();
  return result;
}

struct MatchKey {
  std::uint32_t seed = 0;
  int move_band = 0;
  int rise_phase = 0;
  int occupancy_bin = 0;
  int height_bin = 0;

  auto operator<=>(const MatchKey&) const = default;
};

struct Accumulator {
  Features sum{};
  int count = 0;

  void add(const Features& values) {
    for (std::size_t feature = 0; feature < values.size(); ++feature) {
      sum[feature] += values[feature];
    }
    ++count;
  }

  Features mean() const {
    if (count == 0) throw std::logic_error("empty matched accumulator");
    Features result{};
    for (std::size_t feature = 0; feature < result.size(); ++feature) {
      result[feature] = sum[feature] / count;
    }
    return result;
  }
};

struct SeedPairAccumulator {
  Features exact_sum{};
  Features oracle_sum{};
  int strata = 0;
};

struct SeedPair {
  std::uint32_t seed = 0;
  Features exact{};
  Features oracle{};
  int matched_strata = 0;
};

struct MatchedData {
  std::vector<SeedPair> seeds;
  int matched_strata = 0;
  int exact_records = 0;
  int oracle_records = 0;
};

std::map<MatchKey, Accumulator> aggregateRecords(
    const std::vector<GameResult>& games, int& record_count) {
  std::map<MatchKey, Accumulator> result;
  record_count = 0;
  for (const GameResult& game : games) {
    for (const StateRecord& record : game.records) {
      const MatchKey key{record.seed, record.move_band, record.rise_phase,
                         record.occupancy_bin, record.height_bin};
      result[key].add(record.features);
      ++record_count;
    }
  }
  return result;
}

MatchedData matchTopology(const Cohort& cohort) {
  MatchedData result;
  const auto exact = aggregateRecords(cohort.exact, result.exact_records);
  const auto oracle = aggregateRecords(cohort.oracle, result.oracle_records);
  std::map<std::uint32_t, SeedPairAccumulator> by_seed;
  for (const auto& [key, exact_values] : exact) {
    const auto found = oracle.find(key);
    if (found == oracle.end()) continue;
    const Features exact_mean = exact_values.mean();
    const Features oracle_mean = found->second.mean();
    SeedPairAccumulator& seed = by_seed[key.seed];
    for (std::size_t feature = 0; feature < kFeatureCount; ++feature) {
      seed.exact_sum[feature] += exact_mean[feature];
      seed.oracle_sum[feature] += oracle_mean[feature];
    }
    ++seed.strata;
    ++result.matched_strata;
  }
  result.seeds.reserve(by_seed.size());
  for (const auto& [seed, values] : by_seed) {
    SeedPair pair;
    pair.seed = seed;
    pair.matched_strata = values.strata;
    for (std::size_t feature = 0; feature < kFeatureCount; ++feature) {
      pair.exact[feature] = values.exact_sum[feature] / values.strata;
      pair.oracle[feature] = values.oracle_sum[feature] / values.strata;
    }
    result.seeds.push_back(pair);
  }
  return result;
}

struct FeatureStats {
  int seeds = 0;
  double exact_mean = 0.0;
  double oracle_mean = 0.0;
  double delta = 0.0;
  double standard_deviation = 0.0;
  double effect = 0.0;
  bool effect_defined = false;
};

FeatureStats featureStats(const std::vector<SeedPair>& pairs,
                          std::size_t feature, std::uint32_t begin,
                          std::uint32_t end) {
  FeatureStats result;
  std::vector<double> deltas;
  for (const SeedPair& pair : pairs) {
    if (pair.seed < begin || pair.seed >= end) continue;
    result.exact_mean += pair.exact[feature];
    result.oracle_mean += pair.oracle[feature];
    deltas.push_back(pair.oracle[feature] - pair.exact[feature]);
  }
  result.seeds = static_cast<int>(deltas.size());
  if (deltas.empty()) return result;
  result.exact_mean /= deltas.size();
  result.oracle_mean /= deltas.size();
  result.delta =
      std::accumulate(deltas.begin(), deltas.end(), 0.0) / deltas.size();
  if (deltas.size() > 1) {
    double squared = 0.0;
    for (const double delta : deltas) {
      squared += (delta - result.delta) * (delta - result.delta);
    }
    result.standard_deviation =
        std::sqrt(squared / static_cast<double>(deltas.size() - 1));
    if (result.standard_deviation > 1.0e-12) {
      result.effect = result.delta / result.standard_deviation;
      result.effect_defined = true;
    }
  }
  return result;
}

struct FeatureResult {
  std::size_t feature = 0;
  FeatureStats overall;
  FeatureStats first_half;
  FeatureStats second_half;
  bool direction_stable = false;
  bool stable_hypothesis = false;
  double stability_rank = 0.0;
};

std::vector<FeatureResult> analyzeFeatures(const MatchedData& matched,
                                           const Config& config) {
  const std::uint32_t split =
      config.seed_start + static_cast<std::uint32_t>(config.games / 2);
  const std::uint32_t end =
      config.seed_start + static_cast<std::uint32_t>(config.games);
  std::vector<FeatureResult> result;
  result.reserve(kFeatureCount);
  for (std::size_t feature = 0; feature < kFeatureCount; ++feature) {
    FeatureResult item;
    item.feature = feature;
    item.overall =
        featureStats(matched.seeds, feature, config.seed_start, end);
    item.first_half =
        featureStats(matched.seeds, feature, config.seed_start, split);
    item.second_half = featureStats(matched.seeds, feature, split, end);
    item.direction_stable =
        item.first_half.delta * item.second_half.delta > 0.0;
    const auto strong_enough = [](const FeatureStats& stats) {
      return !stats.effect_defined ||
             std::abs(stats.effect) >= kMinimumStableEffect;
    };
    item.stable_hypothesis =
        item.direction_stable &&
        item.first_half.seeds >= kMinimumSeedsPerHalf &&
        item.second_half.seeds >= kMinimumSeedsPerHalf &&
        strong_enough(item.first_half) && strong_enough(item.second_half);
    const auto magnitude = [](const FeatureStats& stats) {
      return stats.effect_defined ? std::abs(stats.effect) :
                                    (std::abs(stats.delta) > 1.0e-12 ? 99.0
                                                                    : 0.0);
    };
    item.stability_rank =
        std::min(magnitude(item.first_half), magnitude(item.second_half));
    result.push_back(item);
  }
  std::stable_sort(result.begin(), result.end(),
                   [](const FeatureResult& left,
                      const FeatureResult& right) {
                     if (left.stable_hypothesis != right.stable_hypothesis) {
                       return left.stable_hypothesis;
                     }
                     return left.stability_rank > right.stability_rank;
                   });
  return result;
}

struct PolicySummary {
  int games = 0;
  double mean_score = 0.0;
  double mean_moves = 0.0;
  int censored = 0;
  double clears_per_move = 0.0;
  double reveals_per_move = 0.0;
  double waves_per_move = 0.0;
  double mean_maximum_chain_depth = 0.0;
  double work_per_move = 0.0;
  double generated_per_move = 0.0;
  double completed_depth = 0.0;
  double depth_three_rate = 0.0;
  std::size_t peak_candidates = 0;
  double aggregate_game_seconds = 0.0;
};

PolicySummary summarizePolicy(const std::vector<GameResult>& games,
                              std::uint32_t begin, std::uint32_t end) {
  PolicySummary result;
  std::uint64_t moves = 0;
  std::uint64_t clears = 0;
  std::uint64_t reveals = 0;
  std::uint64_t waves = 0;
  std::uint64_t work = 0;
  std::uint64_t generated = 0;
  std::uint64_t depth_sum = 0;
  std::uint64_t depth_three = 0;
  for (const GameResult& game : games) {
    if (game.seed < begin || game.seed >= end) continue;
    ++result.games;
    result.mean_score += game.score;
    result.mean_moves += game.moves;
    result.censored += game.censored;
    result.mean_maximum_chain_depth += game.maximum_chain_depth;
    result.aggregate_game_seconds += game.elapsed_seconds;
    result.peak_candidates =
        std::max(result.peak_candidates, game.peak_candidates);
    moves += game.moves;
    clears += game.cleared;
    reveals += game.revealed;
    waves += game.waves;
    work += game.work;
    generated += game.generated;
    depth_sum += game.completed_depth_sum;
    depth_three += game.completed_depth_three;
  }
  if (result.games == 0) return result;
  result.mean_score /= result.games;
  result.mean_moves /= result.games;
  result.mean_maximum_chain_depth /= result.games;
  const double move_count = static_cast<double>(std::max<std::uint64_t>(1, moves));
  result.clears_per_move = clears / move_count;
  result.reveals_per_move = reveals / move_count;
  result.waves_per_move = waves / move_count;
  result.work_per_move = work / move_count;
  result.generated_per_move = generated / move_count;
  result.completed_depth = depth_sum / move_count;
  result.depth_three_rate = depth_three / move_count;
  return result;
}

struct PairedOutcome {
  int games = 0;
  double score_delta = 0.0;
  double move_delta = 0.0;
  int score_wins = 0;
  int move_wins = 0;
  int score_ties = 0;
  int move_ties = 0;
};

PairedOutcome pairedOutcome(const Cohort& cohort, std::uint32_t begin,
                            std::uint32_t end) {
  if (cohort.exact.size() != cohort.oracle.size()) {
    throw std::logic_error("oracle audit cohort is not paired");
  }
  PairedOutcome result;
  for (std::size_t game = 0; game < cohort.exact.size(); ++game) {
    const GameResult& exact = cohort.exact[game];
    const GameResult& oracle = cohort.oracle[game];
    if (exact.seed != oracle.seed) {
      throw std::logic_error("oracle audit seed pairing failed");
    }
    if (exact.seed < begin || exact.seed >= end) continue;
    ++result.games;
    result.score_delta += oracle.score - exact.score;
    result.move_delta += oracle.moves - exact.moves;
    result.score_wins += oracle.score > exact.score;
    result.move_wins += oracle.moves > exact.moves;
    result.score_ties += oracle.score == exact.score;
    result.move_ties += oracle.moves == exact.moves;
  }
  if (result.games > 0) {
    result.score_delta /= result.games;
    result.move_delta /= result.games;
  }
  return result;
}

void writeOptionalNumber(std::ostream& output, double value, bool defined) {
  if (defined && std::isfinite(value)) output << value;
  else output << "null";
}

void writePolicySummary(std::ostream& output, const PolicySummary& summary) {
  output << "{\"games\":" << summary.games
         << ",\"meanScore\":" << summary.mean_score
         << ",\"meanMoves\":" << summary.mean_moves
         << ",\"censored\":" << summary.censored
         << ",\"clearsPerMove\":" << summary.clears_per_move
         << ",\"revealsPerMove\":" << summary.reveals_per_move
         << ",\"wavesPerMove\":" << summary.waves_per_move
         << ",\"meanMaximumChainDepth\":"
         << summary.mean_maximum_chain_depth
         << ",\"workPerMove\":" << summary.work_per_move
         << ",\"generatedPerMove\":" << summary.generated_per_move
         << ",\"meanCompletedDepth\":" << summary.completed_depth
         << ",\"depthThreeRate\":" << summary.depth_three_rate
         << ",\"peakCandidates\":" << summary.peak_candidates
         << ",\"aggregateGameSeconds\":"
         << summary.aggregate_game_seconds << '}';
}

void writePairedOutcome(std::ostream& output, const PairedOutcome& outcome) {
  output << "{\"games\":" << outcome.games
         << ",\"meanScoreDelta\":" << outcome.score_delta
         << ",\"meanMoveDelta\":" << outcome.move_delta
         << ",\"scoreWins\":" << outcome.score_wins
         << ",\"moveWins\":" << outcome.move_wins
         << ",\"scoreTies\":" << outcome.score_ties
         << ",\"moveTies\":" << outcome.move_ties << '}';
}

void writeFeatureStats(std::ostream& output, const FeatureStats& stats) {
  output << "{\"seeds\":" << stats.seeds
         << ",\"exactMean\":" << stats.exact_mean
         << ",\"oracleMean\":" << stats.oracle_mean
         << ",\"oracleMinusExact\":" << stats.delta
         << ",\"seedDeltaSd\":" << stats.standard_deviation
         << ",\"pairedEffect\":";
  writeOptionalNumber(output, stats.effect, stats.effect_defined);
  output << '}';
}

std::string hexSeed(std::uint32_t seed) {
  std::ostringstream output;
  output << "0x" << std::hex << std::setw(8) << std::setfill('0') << seed;
  return output.str();
}

void writeGames(std::ostream& output, const Cohort& cohort) {
  output << '[';
  for (std::size_t game = 0; game < cohort.exact.size(); ++game) {
    if (game > 0) output << ',';
    const GameResult& exact = cohort.exact[game];
    const GameResult& oracle = cohort.oracle[game];
    output << "{\"seed\":\"" << hexSeed(exact.seed)
           << "\",\"half\":"
           << (game < cohort.exact.size() / 2 ? 1 : 2)
           << ",\"exact\":{\"score\":" << exact.score
           << ",\"moves\":" << exact.moves
           << ",\"censored\":" << (exact.censored ? "true" : "false")
           << ",\"work\":" << exact.work << "},\"oracle\":{\"score\":"
           << oracle.score << ",\"moves\":" << oracle.moves
           << ",\"censored\":" << (oracle.censored ? "true" : "false")
           << ",\"generated\":" << oracle.generated
           << ",\"deduplicated\":" << oracle.deduplicated << "}}";
  }
  output << ']';
}

void writeJson(const Config& config, const Cohort& cohort,
               const MatchedData& matched,
               const std::vector<FeatureResult>& features) {
  const std::uint32_t split =
      config.seed_start + static_cast<std::uint32_t>(config.games / 2);
  const std::uint32_t end =
      config.seed_start + static_cast<std::uint32_t>(config.games);
  const PolicySummary exact_all =
      summarizePolicy(cohort.exact, config.seed_start, end);
  const PolicySummary oracle_all =
      summarizePolicy(cohort.oracle, config.seed_start, end);
  const PolicySummary exact_first =
      summarizePolicy(cohort.exact, config.seed_start, split);
  const PolicySummary oracle_first =
      summarizePolicy(cohort.oracle, config.seed_start, split);
  const PolicySummary exact_second =
      summarizePolicy(cohort.exact, split, end);
  const PolicySummary oracle_second =
      summarizePolicy(cohort.oracle, split, end);
  std::ofstream output(config.json);
  if (!output) throw std::runtime_error("could not open oracle audit JSON");
  output << std::setprecision(10)
         << "{\n  \"format\":\"drop7-oracle-topology-audit-v1\",\n"
         << "  \"warning\":\"analysis-only privileged oracle; never deploy oracle-dependent inputs\",\n"
         << "  \"mechanics\":{\"levelBonus\":7000},\n"
         << "  \"seedPolicy\":{\"trainingOnly\":true,\"forbiddenFamiliesInspected\":false,"
            "\"seedStart\":\""
         << hexSeed(config.seed_start) << "\",\"seedEndExclusive\":\""
         << hexSeed(end) << "\",\"sameSeedsAcrossPolicies\":true},\n"
         << "  \"configuration\":{\"gamesPerPolicy\":" << config.games
         << ",\"maximumMoves\":" << config.maximum_moves
         << ",\"parallelism\":" << config.parallelism
         << ",\"exact\":{\"depth\":3,\"chanceSamples\":5,"
            "\"maximumWork\":1000000,\"maximumCacheEntries\":40000,"
            "\"publicStateOnly\":true},\"oracle\":{\"privilegedFutureAware\":true,"
            "\"depth\":"
         << config.oracle_depth << ",\"beamWidth\":" << config.oracle_beam
         << "}},\n"
         << "  \"matching\":{\"unit\":\"same-seed topology stratum then one mean delta per seed\","
            "\"moveBandWidth\":"
         << kMoveBandWidth << ",\"risePhase\":\"exact movesRemaining 1..5\","
            "\"occupancyBinWidth\":"
         << kOccupancyBinWidth << ",\"maximumHeightBinWidth\":"
         << kHeightBinWidth << ",\"exactRecords\":"
         << matched.exact_records << ",\"oracleRecords\":"
         << matched.oracle_records << ",\"matchedStrata\":"
         << matched.matched_strata << ",\"matchedSeeds\":"
         << matched.seeds.size() << "},\n"
         << "  \"stabilityRule\":{\"seedHalves\":[\""
         << hexSeed(config.seed_start) << ".." << hexSeed(split - 1)
         << "\",\"" << hexSeed(split) << ".." << hexSeed(end - 1)
         << "\"],\"minimumSeedsPerHalf\":" << kMinimumSeedsPerHalf
         << ",\"sameNonzeroDirectionRequired\":true,"
            "\"minimumAbsoluteEffectEachHalf\":"
         << kMinimumStableEffect << "},\n"
         << "  \"outcomes\":{\"overall\":{\"exact\":";
  writePolicySummary(output, exact_all);
  output << ",\"oracle\":";
  writePolicySummary(output, oracle_all);
  output << ",\"pairedOracleMinusExact\":";
  writePairedOutcome(output, pairedOutcome(cohort, config.seed_start, end));
  output << "},\"firstHalf\":{\"exact\":";
  writePolicySummary(output, exact_first);
  output << ",\"oracle\":";
  writePolicySummary(output, oracle_first);
  output << ",\"pairedOracleMinusExact\":";
  writePairedOutcome(output,
                     pairedOutcome(cohort, config.seed_start, split));
  output << "},\"secondHalf\":{\"exact\":";
  writePolicySummary(output, exact_second);
  output << ",\"oracle\":";
  writePolicySummary(output, oracle_second);
  output << ",\"pairedOracleMinusExact\":";
  writePairedOutcome(output, pairedOutcome(cohort, split, end));
  output << "}},\n  \"games\":";
  writeGames(output, cohort);
  output << ",\n  \"features\":[";
  for (std::size_t index = 0; index < features.size(); ++index) {
    if (index > 0) output << ',';
    const FeatureResult& feature = features[index];
    const FeatureSpec& spec = kFeatureSpecs[feature.feature];
    output << "{\"name\":\"" << spec.name << "\",\"category\":\""
           << spec.category << "\",\"kind\":\"" << spec.kind
           << "\",\"interpretation\":\"" << spec.interpretation
           << "\",\"overall\":";
    writeFeatureStats(output, feature.overall);
    output << ",\"firstHalf\":";
    writeFeatureStats(output, feature.first_half);
    output << ",\"secondHalf\":";
    writeFeatureStats(output, feature.second_half);
    output << ",\"directionStable\":"
           << (feature.direction_stable ? "true" : "false")
           << ",\"stableHypothesis\":"
           << (feature.stable_hypothesis ? "true" : "false")
           << ",\"candidateDirection\":\""
           << (feature.overall.delta > 0 ? "higher" : "lower")
           << "\",\"stabilityRank\":" << feature.stability_rank << '}';
  }
  output << "],\n  \"limits\":["
            "\"oracle sees realized future random streams and is not deployable\","
            "\"beam pruning makes the oracle an approximate privileged upper bound\","
            "\"matched observational differences are hypotheses, not causal feature weights\","
            "\"transition-outcome features require a separate public-state predictor before policy use\","
            "\"correlated features and survivor selection remain after coarse topology matching\"],\n"
         << "  \"wallSeconds\":" << cohort.wall_seconds << "\n}\n";
}

std::string formatNumber(double value, int precision = 3) {
  std::ostringstream output;
  output << std::fixed << std::setprecision(precision) << value;
  return output.str();
}

std::string formatEffect(const FeatureStats& stats) {
  return stats.effect_defined ? formatNumber(stats.effect, 2) : "n/a";
}

void writeReadme(const Config& config, const Cohort& cohort,
                 const MatchedData& matched,
                 const std::vector<FeatureResult>& features) {
  const std::uint32_t end =
      config.seed_start + static_cast<std::uint32_t>(config.games);
  const PolicySummary exact =
      summarizePolicy(cohort.exact, config.seed_start, end);
  const PolicySummary oracle =
      summarizePolicy(cohort.oracle, config.seed_start, end);
  const PairedOutcome paired =
      pairedOutcome(cohort, config.seed_start, end);
  std::ofstream output(config.readme);
  if (!output) throw std::runtime_error("could not open oracle audit README");
  output << "# Drop7 oracle-topology audit\n\n"
         << "> Analysis only. The oracle sees the realized future disc and reveal streams. "
            "It is neither fair nor deployable, and none of its privileged inputs may enter a policy.\n\n"
         << "## Scope and method\n\n"
         << "The audit ran " << config.games << " exact-depth-3 games and "
         << config.games << " privileged depth-" << config.oracle_depth
         << ", beam-" << config.oracle_beam << " games on the same fresh "
         << hexSeed(config.seed_start) << "–" << hexSeed(end - 1)
         << " training seeds. Mechanics use the corrected 7,000-point level bonus. "
            "No `0x7d` or `0xd7` seed was inspected.\n\n"
         << "States were matched within the same seed, 20-move band, exact rise phase, "
            "four-cell occupancy bin, and two-row maximum-height bin. Each matched topology "
            "stratum was averaged first, then each seed contributed one mean delta so repeated "
            "states do not masquerade as independent games. There were "
         << matched.matched_strata << " matched strata across "
         << matched.seeds.size() << " seeds (" << matched.exact_records
         << " exact states and " << matched.oracle_records
         << " oracle states before matching).\n\n"
         << "A feature is called direction-stable only when both eight-seed halves have "
            "the same nonzero oracle-minus-exact direction, at least six matched seeds, and "
            "an absolute paired effect of at least 0.10 in each half. This is a hypothesis "
            "filter, not a significance or causality claim.\n\n"
         << "## Outcomes\n\n"
         << "| Policy | Games | Mean score | Mean moves | Clears/move | Reveals/move | Waves/move | Capped |\n"
            "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |\n"
         << "| Exact d3 | " << exact.games << " | "
         << formatNumber(exact.mean_score, 0) << " | "
         << formatNumber(exact.mean_moves, 2) << " | "
         << formatNumber(exact.clears_per_move) << " | "
         << formatNumber(exact.reveals_per_move) << " | "
         << formatNumber(exact.waves_per_move) << " | " << exact.censored
         << " |\n"
         << "| Privileged oracle | " << oracle.games << " | "
         << formatNumber(oracle.mean_score, 0) << " | "
         << formatNumber(oracle.mean_moves, 2) << " | "
         << formatNumber(oracle.clears_per_move) << " | "
         << formatNumber(oracle.reveals_per_move) << " | "
         << formatNumber(oracle.waves_per_move) << " | " << oracle.censored
         << " |\n\n"
         << "Paired oracle-minus-exact outcome: "
         << (paired.score_delta >= 0 ? "+" : "")
         << formatNumber(paired.score_delta, 0) << " points and "
         << (paired.move_delta >= 0 ? "+" : "")
         << formatNumber(paired.move_delta, 2) << " moves; score wins "
         << paired.score_wins << '/' << paired.games << ", move wins "
         << paired.move_wins << '/' << paired.games << ". Exact search completed "
         << formatNumber(100.0 * exact.depth_three_rate, 2)
         << "% of depth-3 iterations.\n\n"
         << "## Direction-stable topology signals\n\n"
         << "Positive deltas mean the matched oracle states/outcomes are higher. "
            "Transition outcomes describe the following realized move and cannot be used "
            "directly by a fair state heuristic.\n\n"
         << "| Feature | Category | Kind | Exact | Oracle | Delta | Effect H1 | Effect H2 | Candidate direction |\n"
            "| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | --- |\n";
  int stable_count = 0;
  for (const FeatureResult& feature : features) {
    if (!feature.stable_hypothesis) continue;
    ++stable_count;
    const FeatureSpec& spec = kFeatureSpecs[feature.feature];
    output << "| " << spec.name << " | " << spec.category << " | "
           << spec.kind << " | " << formatNumber(feature.overall.exact_mean)
           << " | " << formatNumber(feature.overall.oracle_mean) << " | "
           << (feature.overall.delta >= 0 ? "+" : "")
           << formatNumber(feature.overall.delta) << " | "
           << formatEffect(feature.first_half) << " | "
           << formatEffect(feature.second_half) << " | "
           << (feature.overall.delta > 0 ? "higher" : "lower") << " |\n";
  }
  if (stable_count == 0) {
    output << "| _None passed the split-half rule_ | | | | | | | | |\n";
  }
  output << "\n## Next heuristic hypothesis\n\n";
  for (const std::string_view category : {
           "highNumberTrench", "coverAccess", "storedEnergy",
           "coverAltitudeDamage", "lowNumberClog",
           "clearRevealThroughput"}) {
    int emitted = 0;
    for (const FeatureResult& feature : features) {
      const FeatureSpec& spec = kFeatureSpecs[feature.feature];
      if (!feature.stable_hypothesis || spec.category != category) continue;
      if (emitted == 0) output << "- **" << category << ":** ";
      else output << "; ";
      output << (feature.overall.delta > 0 ? "retain/reward higher "
                                          : "penalize/reduce ")
             << spec.name;
      ++emitted;
      if (emitted == 3) break;
    }
    if (emitted > 0) output << ".\n";
  }
  output << "\nThe safest next experiment is a fair exact-d3 leaf ablation containing only "
            "the split-stable **state** signals above, with signs fixed by this audit. "
            "Keep transition-throughput signals as supervised prediction targets rather "
            "than rewards until a public-state predictor validates out of sample.\n\n"
         << "## Limits\n\n"
         << "- The oracle is an approximate privileged upper bound: beam pruning can discard "
            "the globally best future path.\n"
         << "- Matching reduces obvious phase/load confounding but does not remove survivor "
            "selection, feature correlation, or policy-induced state-distribution shift.\n"
         << "- Sixteen paired seeds are adequate for hypothesis generation, not promotion. "
            "Every proposed feature still needs a fresh fair-policy ablation.\n";
}

int positiveInteger(std::string_view text, std::string_view flag) {
  std::size_t consumed = 0;
  const unsigned long long value =
      std::stoull(std::string(text), &consumed, 0);
  if (consumed != text.size() || value == 0 ||
      value > static_cast<unsigned long long>(
                  std::numeric_limits<int>::max())) {
    throw std::invalid_argument(std::string(flag) +
                                " must be a positive integer");
  }
  return static_cast<int>(value);
}

std::uint32_t parseSeed(std::string_view text) {
  std::size_t consumed = 0;
  const unsigned long long value =
      std::stoull(std::string(text), &consumed, 0);
  if (consumed != text.size() ||
      value > std::numeric_limits<std::uint32_t>::max()) {
    throw std::invalid_argument("seed must be a uint32 integer");
  }
  return static_cast<std::uint32_t>(value);
}

void validateConfig(Config& config) {
  if (config.games < 12 || config.games % 2 != 0) {
    throw std::invalid_argument(
        "oracle audit games must be an even number of at least twelve");
  }
  if (config.maximum_moves < 20 || config.maximum_moves > 1'000) {
    throw std::invalid_argument("maximum moves must be from 20 to 1000");
  }
  if (config.oracle_depth < 1 || config.oracle_depth > 12 ||
      config.oracle_beam < 1 || config.oracle_beam > 2'048) {
    throw std::invalid_argument("oracle bounds are invalid");
  }
  config.parallelism = std::min(config.parallelism, 16);
  const std::uint64_t end =
      static_cast<std::uint64_t>(config.seed_start) + config.games;
  if (config.seed_start < kTrainingStart || end > kTrainingEnd ||
      (config.seed_start & 0xff00'0000u) != 0x3d00'0000u) {
    throw std::invalid_argument(
        "oracle audit is restricted to the 0x3d training family");
  }
}

Config parseConfig(int argc, char** argv) {
  Config config;
  for (int index = 2; index < argc; ++index) {
    const std::string flag = argv[index];
    if (index + 1 >= argc) {
      throw std::invalid_argument("missing value for " + flag);
    }
    const std::string_view value = argv[++index];
    if (flag == "--seed-start") {
      config.seed_start = parseSeed(value);
    } else if (flag == "--games") {
      config.games = positiveInteger(value, flag);
    } else if (flag == "--max-moves") {
      config.maximum_moves = positiveInteger(value, flag);
    } else if (flag == "--oracle-depth") {
      config.oracle_depth = positiveInteger(value, flag);
    } else if (flag == "--oracle-beam") {
      config.oracle_beam = positiveInteger(value, flag);
    } else if (flag == "--parallel") {
      config.parallelism = positiveInteger(value, flag);
    } else if (flag == "--json") {
      config.json = value;
    } else if (flag == "--readme") {
      config.readme = value;
    } else {
      throw std::invalid_argument("unknown oracle audit argument " + flag);
    }
  }
  validateConfig(config);
  return config;
}

bool selfTest(std::ostream& output) {
  constexpr std::uint32_t seed = 0x3d70'ff00u;
  State state = initialHeadlessState(seed);
  const OraclePlan first = planOracleMove(state, seed, 3, 16);
  const OraclePlan repeat = planOracleMove(state, seed, 3, 16);
  const bool oracle_deterministic =
      first.column == repeat.column &&
      first.stats.generated == repeat.stats.generated &&
      first.stats.deduplicated == repeat.stats.deduplicated;
  const bool oracle_legal = isLegal(state.board, first.column);

  cfpi::BehaviorOptions exact;
  exact.max_depth = 3;
  exact.chance_samples = 5;
  exact.max_work = 1'000'000;
  exact.max_cache_entries = 40'000;
  cfpi::BehaviorMetrics exact_metrics;
  const int exact_action =
      cfpi::chooseBehaviorAction(publicState(state), exact, &exact_metrics);
  State metadata_changed = state;
  metadata_changed.score = 999'999;
  metadata_changed.level = 77;
  metadata_changed.moves_played = 444;
  const int metadata_action =
      cfpi::chooseBehaviorAction(publicState(metadata_changed), exact);
  const bool public_exact = exact_action == metadata_action &&
                            exact_metrics.completed_depth == 3 &&
                            isLegal(state.board, exact_action);

  const State before = state;
  MoveResult move;
  const bool moved = playHeadlessMove(state, seed, first.column, move);
  const Features features = extractFeatures(before, move);
  State mirrored = before;
  mirrored.board = cfpi::detail::mirrorBoard(before.board);
  const Features reflected = extractFeatures(mirrored, move);
  bool reflection_safe = true;
  bool finite = true;
  for (std::size_t feature = 0; feature < kFeatureCount; ++feature) {
    reflection_safe =
        reflection_safe && std::abs(features[feature] - reflected[feature]) <
                               1.0e-9;
    finite = finite && std::isfinite(features[feature]);
  }
  const bool passed = kLevelBonus == 7'000 && oracle_deterministic &&
                      oracle_legal && public_exact && moved &&
                      reflection_safe && finite &&
                      kFeatureSpecs.size() == kFeatureCount;
  output << "ORACLE_TOPOLOGY_SELF_TEST {\"passed\":"
         << (passed ? "true" : "false")
         << ",\"levelBonus\":" << kLevelBonus
         << ",\"oracleDeterministic\":"
         << (oracle_deterministic ? "true" : "false")
         << ",\"oracleLegal\":" << (oracle_legal ? "true" : "false")
         << ",\"publicExactD3\":"
         << (public_exact ? "true" : "false")
         << ",\"reflectionSafe\":"
         << (reflection_safe ? "true" : "false")
         << ",\"finite\":" << (finite ? "true" : "false")
         << ",\"trainingSeedOnly\":true}\n";
  return passed;
}

int run(const Config& config, std::ostream& output) {
  const Cohort cohort = runCohort(config);
  const MatchedData matched = matchTopology(cohort);
  const std::vector<FeatureResult> features =
      analyzeFeatures(matched, config);
  writeJson(config, cohort, matched, features);
  writeReadme(config, cohort, matched, features);
  int stable = 0;
  for (const FeatureResult& feature : features) {
    stable += feature.stable_hypothesis;
  }
  const std::uint32_t end =
      config.seed_start + static_cast<std::uint32_t>(config.games);
  const PolicySummary exact =
      summarizePolicy(cohort.exact, config.seed_start, end);
  const PolicySummary oracle =
      summarizePolicy(cohort.oracle, config.seed_start, end);
  const PairedOutcome paired =
      pairedOutcome(cohort, config.seed_start, end);
  output << std::fixed << std::setprecision(3)
         << "ORACLE_TOPOLOGY_RESULT {\"trainingSeedOnly\":true"
         << ",\"gamesPerPolicy\":" << config.games
         << ",\"exactScore\":" << exact.mean_score
         << ",\"exactMoves\":" << exact.mean_moves
         << ",\"oracleScore\":" << oracle.mean_score
         << ",\"oracleMoves\":" << oracle.mean_moves
         << ",\"scoreDelta\":" << paired.score_delta
         << ",\"moveDelta\":" << paired.move_delta
         << ",\"matchedStrata\":" << matched.matched_strata
         << ",\"matchedSeeds\":" << matched.seeds.size()
         << ",\"stableFeatures\":" << stable
         << ",\"json\":\"" << config.json << "\",\"readme\":\""
         << config.readme << "\"}\n";
  return 0;
}

}  // namespace drop7::oracle_topology

#ifndef DROP7_ORACLE_TOPOLOGY_LIBRARY
int main(int argc, char** argv) {
  try {
    if (argc == 2 && std::string(argv[1]) == "--self-test") {
      return drop7::oracle_topology::selfTest(std::cout) ? 0 : 1;
    }
    if (argc >= 2 && std::string(argv[1]) == "--run") {
      return drop7::oracle_topology::run(
          drop7::oracle_topology::parseConfig(argc, argv), std::cout);
    }
    std::cerr
        << "usage: drop7_oracle_topology_audit --self-test | --run "
           "[--seed-start 0x3d...] [--games EVEN>=12] [--max-moves N] "
           "[--oracle-depth N] [--oracle-beam N] [--parallel N] "
           "[--json PATH] [--readme PATH]\n";
    return 2;
  } catch (const std::exception& error) {
    std::cerr << "drop7_oracle_topology_audit: " << error.what() << '\n';
    return 1;
  }
}
#endif