Drop7 Research
approaches/constructive-reservoir/constructive-spectrum/constructive-horizon-scale.cppC++1,016 lines · 41.2 KB
#define main drop7_constructive_spectrum_frozen_entrypoint
#include "constructive-spectrum.cpp"
#undef main

#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <mutex>
#include <numeric>
#include <stdexcept>
#include <string>
#include <string_view>
#include <sys/resource.h>
#include <type_traits>
#include <utility>
#include <vector>

// Isolates only constructive rollout horizon length.  H7 delegates to the
// fixed policy above.  Longer variants reuse the exact fixed D3 shield,
// target, reward, seven stratified scenarios, one-step continuation, sampling
// domains, 2,500-unit top-two margin, and center-first tie order.
namespace drop7::constructive_horizon_scale {

namespace frozen = drop7::constructive_spectrum;
namespace fair = drop7::fair_only_horizon;
namespace detail = drop7::cfpi::detail;
using Clock = std::chrono::steady_clock;
using PublicState = frozen::PublicState;
using Decision = frozen::Decision;

constexpr std::uint32_t kFittingSeedStart = 0x3d6a'4000u;
constexpr std::uint32_t kFittingSeedEndExclusive = 0x3d6a'4020u;
constexpr int kFittingGames = 32;
constexpr std::uint32_t kScreenSeedStart = 0x3d6a'5000u;
constexpr std::uint32_t kScreenSeedEndExclusive = 0x3d6a'5040u;
constexpr int kScreenGames = 64;
constexpr int kMaximumMoves = 1'000;
constexpr int kDefaultThreads = 8;
constexpr double kWallLimitSeconds = 45.0 * 60.0;
constexpr std::uint64_t kRssLimitBytes = 256ull * 1024ull * 1024ull;
constexpr double kRequiredRatio = 1.10;
constexpr int kFitJointWins = 20;
constexpr int kScreenJointWins = 40;

static_assert(kLevelBonus == 17'000);
static_assert(kMovesPerLevel == 5);
static_assert(frozen::kChanceSamples == 7);
static_assert(frozen::kMinimumHorizon == 3 &&
              frozen::kMaximumHorizon == 7);
static_assert(frozen::kTacticalDepth == 3 &&
              frozen::kTacticalShortlist == 2 &&
              frozen::kTacticalNearTie == 2'500.0);
static_assert(frozen::kPolicySeed == 0x4353'5031u);
static_assert(frozen::kTerminalValue == -1.0e9);
static_assert(kFittingSeedEndExclusive - kFittingSeedStart == kFittingGames);
static_assert(kScreenSeedEndExclusive - kScreenSeedStart == kScreenGames);
static_assert(kFittingSeedEndExclusive <= kScreenSeedStart);
static_assert((kFittingSeedStart >> 16u) == 0x3d6au &&
              ((kFittingSeedEndExclusive - 1u) >> 16u) == 0x3d6au &&
              (kScreenSeedStart >> 16u) == 0x3d6au &&
              ((kScreenSeedEndExclusive - 1u) >> 16u) == 0x3d6au);
static_assert((kFittingSeedStart >> 24u) != 0x4du &&
              (kFittingSeedStart >> 24u) != 0x7du &&
              (kFittingSeedStart >> 24u) != 0xd7u &&
              (kScreenSeedStart >> 24u) != 0x4du &&
              (kScreenSeedStart >> 24u) != 0x7du &&
              (kScreenSeedStart >> 24u) != 0xd7u);

enum class Variant : std::uint8_t { kH7, kH12, kH17, kH27, kCount };

constexpr std::array<Variant, 4> kVariants{{
    Variant::kH7, Variant::kH12, Variant::kH17, Variant::kH27}};

std::string_view variantName(Variant variant) {
  switch (variant) {
    case Variant::kH7:
      return "H7";
    case Variant::kH12:
      return "H12";
    case Variant::kH17:
      return "H17";
    case Variant::kH27:
      return "H27";
    case Variant::kCount:
      break;
  }
  throw std::invalid_argument("invalid horizon variant");
}

Variant parseVariant(std::string_view name) {
  for (const Variant variant : kVariants) {
    if (variantName(variant) == name) return variant;
  }
  throw std::invalid_argument("invalid selected horizon variant");
}

int maximumHorizon(Variant variant) {
  switch (variant) {
    case Variant::kH7:
      return 7;
    case Variant::kH12:
      return 12;
    case Variant::kH17:
      return 17;
    case Variant::kH27:
      return 27;
    case Variant::kCount:
      break;
  }
  throw std::invalid_argument("invalid horizon variant");
}

int fullCycles(Variant variant) {
  switch (variant) {
    case Variant::kH7:
      return 1;
    case Variant::kH12:
      return 2;
    case Variant::kH17:
      return 3;
    case Variant::kH27:
      return 5;
    case Variant::kCount:
      break;
  }
  throw std::invalid_argument("invalid horizon variant");
}

int horizonFor(const PublicState& state, Variant variant) {
  return std::clamp(static_cast<int>(state.moves_remaining) +
                        fullCycles(variant) * kMovesPerLevel,
                    frozen::kMinimumHorizon, maximumHorizon(variant));
}

std::uint64_t peakRssBytes() {
  rusage usage{};
  if (getrusage(RUSAGE_SELF, &usage) != 0) return 0;
#if defined(__APPLE__)
  return static_cast<std::uint64_t>(usage.ru_maxrss);
#else
  return static_cast<std::uint64_t>(usage.ru_maxrss) * 1024ull;
#endif
}

void enforceRssLimit() {
  if (peakRssBytes() > kRssLimitBytes) {
    throw std::runtime_error("horizon scale exceeded 256 MiB RSS cap");
  }
}

struct Deadline {
  Clock::time_point started = Clock::now();

  double seconds() const {
    return std::chrono::duration<double>(Clock::now() - started).count();
  }

  void check() const {
    if (seconds() > kWallLimitSeconds) {
      throw std::runtime_error("horizon scale exceeded 45 minute wall cap");
    }
  }
};

Decision chooseActionCanonical(const PublicState& source, Variant variant) {
  if (variant == Variant::kH7) return frozen::chooseActionCanonical(source);
  Decision result;
  result.values.fill(-std::numeric_limits<double>::infinity());
  if (source.terminal) return result;
  const State root = frozen::materialize(source);
  fair::SearchContext tactical_context;
  const fair::RootEvaluation tactical =
      fair::rootDecision(root, frozen::kTacticalDepth, tactical_context);
  result.work += tactical_context.work;
  result.tactical_action = tactical.action;
  std::array<int, kBoardSize> tactical_rank{};
  tactical_rank.fill(kBoardSize);
  std::array<int, kBoardSize> ranked_columns{};
  int ranked_count = 0;
  for (const int column : frozen::kColumnOrder) {
    if (isLegal(root.board, column)) ranked_columns[ranked_count++] = column;
  }
  std::stable_sort(ranked_columns.begin(), ranked_columns.begin() + ranked_count,
                   [&](int left, int right) {
                     return tactical.values[left] > tactical.values[right];
                   });
  for (int rank = 0; rank < ranked_count; ++rank) {
    tactical_rank[ranked_columns[rank]] = rank;
  }
  result.horizon = horizonFor(source, variant);
  for (const int root_column : frozen::kColumnOrder) {
    if (!isLegal(root.board, root_column)) continue;
    if (tactical_rank[root_column] >= frozen::kTacticalShortlist) continue;
    if (tactical.values[root_column] <
        tactical.value - frozen::kTacticalNearTie) {
      continue;
    }
    ++result.shortlist;
    double root_total = 0.0;
    for (int root_sample = 0; root_sample < frozen::kChanceSamples;
         ++root_sample) {
      const frozen::SampledStep first = frozen::sampledStep(
          root, root_column, root_sample, result.horizon);
      ++result.work;
      if (!first.played || first.state.game_over) {
        root_total += frozen::kTerminalValue;
        continue;
      }
      State state = first.state;
      double trajectory = static_cast<double>(first.score_delta) +
                          5'000.0 * first.clears +
                          8'000.0 * first.reveals + 500.0 * first.waves;
      bool terminal = false;
      for (int step_index = 1; step_index < result.horizon; ++step_index) {
        const int depth_tag = result.horizon - step_index;
        const frozen::OneStepDecision continuation =
            frozen::constructiveContinuation(state, depth_tag);
        result.work += continuation.work;
        if (continuation.action < 0) {
          terminal = true;
          break;
        }
        const int sample =
            (root_sample + 2 * step_index) % frozen::kChanceSamples;
        const frozen::SampledStep next = frozen::sampledStep(
            state, continuation.action, sample, depth_tag);
        ++result.work;
        if (!next.played || next.state.game_over) {
          terminal = true;
          break;
        }
        trajectory += static_cast<double>(next.score_delta) +
                      5'000.0 * next.clears + 8'000.0 * next.reveals +
                      500.0 * next.waves;
        state = next.state;
      }
      root_total += terminal
                        ? frozen::kTerminalValue
                        : trajectory +
                              frozen::structuralValue(
                                  frozen::publicState(state));
    }
    result.values[root_column] = root_total / frozen::kChanceSamples;
    if (result.action < 0 ||
        result.values[root_column] > result.values[result.action]) {
      result.action = root_column;
    }
  }
  if (result.action < 0) result.action = centerFirstMove(root.board);
  return result;
}

Decision chooseAction(const PublicState& source, Variant variant) {
  if (variant == Variant::kH7) return frozen::chooseAction(source);
  if (source.terminal) return {};
  bool mirrored = false;
  const PublicState canonical = frozen::canonicalPublic(source, mirrored);
  Decision result = chooseActionCanonical(canonical, variant);
  if (!mirrored) return result;
  result.action = kBoardSize - 1 - result.action;
  result.tactical_action = kBoardSize - 1 - result.tactical_action;
  std::array<double, kBoardSize> values{};
  for (int column = 0; column < kBoardSize; ++column) {
    values[column] = result.values[kBoardSize - 1 - column];
  }
  result.values = values;
  return result;
}

using VariantPolicy = Decision (*)(const PublicState&, Variant);
static_assert(std::is_same_v<decltype(&chooseAction), VariantPolicy>);
static_assert(!std::is_invocable_v<VariantPolicy, const State&, Variant>);

bool allowedFittingSeed(std::uint32_t seed) {
  return seed >= kFittingSeedStart && seed < kFittingSeedEndExclusive;
}

bool allowedScreenSeed(std::uint32_t seed) {
  return seed >= kScreenSeedStart && seed < kScreenSeedEndExclusive;
}

void requireSeed(std::uint32_t seed, bool screen) {
  if (screen ? !allowedScreenSeed(seed) : !allowedFittingSeed(seed)) {
    throw std::invalid_argument(
        screen ? "seed outside exact 0x3d6a5000 screen bank"
               : "seed outside exact 0x3d6a4000 fitting bank");
  }
}

struct GameResult {
  std::uint32_t seed = 0;
  std::int64_t score = 0;
  int moves = 0;
  int clears = 0;
  int reveals = 0;
  int waves = 0;
  int maximum_chain = 0;
  bool natural_terminal = false;
  bool capped = false;
  std::uint64_t work = 0;
  std::uint64_t disc_hash = 0xcbf2'9ce4'8422'2325ull;
};

GameResult playGame(std::uint32_t seed, Variant variant,
                    const Deadline& deadline, bool screen) {
  requireSeed(seed, screen);
  State state = initialHeadlessState(seed);
  GameResult result;
  result.seed = seed;
  while (!state.game_over && state.moves_played < kMaximumMoves) {
    deadline.check();
    enforceRssLimit();
    if (state.next_disc != headlessDisc(seed, state.moves_played)) {
      throw std::runtime_error("headless disc stream guard failed");
    }
    result.disc_hash ^= state.next_disc;
    result.disc_hash *= 0x0000'0100'0000'01b3ull;
    const Decision decision = chooseAction(frozen::publicState(state), variant);
    if (!isLegal(state.board, decision.action)) {
      throw std::runtime_error("horizon policy selected illegal action");
    }
    result.work += decision.work;
    MoveResult move;
    if (!playHeadlessMove(state, seed, decision.action, move)) {
      throw std::runtime_error("headless transition failed");
    }
    result.waves += static_cast<int>(move.waves.size());
    for (const Wave& wave : move.waves) {
      result.clears += wave.cleared;
      result.reveals += wave.revealed;
      result.maximum_chain = std::max(result.maximum_chain, wave.depth);
    }
  }
  result.score = state.score;
  result.moves = state.moves_played;
  result.natural_terminal = state.game_over;
  result.capped = !state.game_over && state.moves_played == kMaximumMoves;
  return result;
}

struct Summary {
  double mean_score = 0.0;
  double mean_moves = 0.0;
  double bottom_quartile_score = 0.0;
  double bottom_quartile_moves = 0.0;
  double clears_per_move = 0.0;
  double reveals_per_move = 0.0;
  double waves_per_move = 0.0;
  double log_objective = 0.0;
  double geometric_objective = 0.0;
  int natural_terminals = 0;
  int capped = 0;
  int maximum_chain = 0;
  std::uint64_t work = 0;
};

Summary summarize(const std::vector<GameResult>& games) {
  if (games.empty()) throw std::invalid_argument("cannot summarize no games");
  Summary result;
  std::vector<std::int64_t> ordered_scores;
  std::vector<int> ordered_moves;
  std::int64_t scores = 0;
  std::int64_t moves = 0;
  std::int64_t clears = 0;
  std::int64_t reveals = 0;
  std::int64_t waves = 0;
  for (const GameResult& game : games) {
    scores += game.score;
    moves += game.moves;
    clears += game.clears;
    reveals += game.reveals;
    waves += game.waves;
    ordered_scores.push_back(game.score);
    ordered_moves.push_back(game.moves);
    result.natural_terminals += game.natural_terminal;
    result.capped += game.capped;
    result.maximum_chain = std::max(result.maximum_chain, game.maximum_chain);
    result.work += game.work;
  }
  std::sort(ordered_scores.begin(), ordered_scores.end());
  std::sort(ordered_moves.begin(), ordered_moves.end());
  const std::size_t quartile =
      std::max<std::size_t>(1, games.size() / 4);
  result.bottom_quartile_score = std::accumulate(
      ordered_scores.begin(), ordered_scores.begin() + quartile, 0.0) /
      quartile;
  result.bottom_quartile_moves = std::accumulate(
      ordered_moves.begin(), ordered_moves.begin() + quartile, 0.0) /
      quartile;
  result.mean_score = static_cast<double>(scores) / games.size();
  result.mean_moves = static_cast<double>(moves) / games.size();
  result.clears_per_move = static_cast<double>(clears) / moves;
  result.reveals_per_move = static_cast<double>(reveals) / moves;
  result.waves_per_move = static_cast<double>(waves) / moves;
  if (result.mean_score <= 0 || result.mean_moves <= 0 ||
      result.bottom_quartile_score <= 0 ||
      result.bottom_quartile_moves <= 0) {
    throw std::runtime_error("log objective received nonpositive statistic");
  }
  result.log_objective =
      (std::log(result.mean_score) + std::log(result.mean_moves) +
       std::log(result.bottom_quartile_score) +
       std::log(result.bottom_quartile_moves)) /
      4.0;
  result.geometric_objective = std::exp(result.log_objective);
  return result;
}

struct Paired {
  int score_wins = 0;
  int move_wins = 0;
  int joint_wins = 0;
  double mean_score_delta = 0.0;
  double mean_move_delta = 0.0;
};

Paired pair(const std::vector<GameResult>& candidate,
            const std::vector<GameResult>& baseline) {
  if (candidate.size() != baseline.size()) {
    throw std::invalid_argument("paired cohorts differ in size");
  }
  Paired result;
  for (std::size_t index = 0; index < candidate.size(); ++index) {
    if (candidate[index].seed != baseline[index].seed) {
      throw std::runtime_error("paired seed mismatch");
    }
    const bool score_win = candidate[index].score > baseline[index].score;
    const bool move_win = candidate[index].moves > baseline[index].moves;
    result.score_wins += score_win;
    result.move_wins += move_win;
    result.joint_wins += score_win && move_win;
    result.mean_score_delta += candidate[index].score - baseline[index].score;
    result.mean_move_delta += candidate[index].moves - baseline[index].moves;
  }
  result.mean_score_delta /= candidate.size();
  result.mean_move_delta /= candidate.size();
  return result;
}

std::vector<GameResult> evaluate(std::uint32_t seed_start, int games,
                                 Variant variant, int threads,
                                 const Deadline& deadline, bool screen) {
  std::vector<GameResult> result(games);
  std::atomic<int> next{0};
  std::mutex progress;
  std::vector<std::future<void>> workers;
  for (int worker = 0; worker < std::min(threads, games); ++worker) {
    workers.push_back(std::async(std::launch::async, [&] {
      for (;;) {
        const int index = next.fetch_add(1);
        if (index >= games) return;
        const std::uint32_t seed = seed_start + index;
        result[index] = playGame(seed, variant, deadline, screen);
        const std::lock_guard<std::mutex> lock(progress);
        std::cerr << variantName(variant) << " seed 0x" << std::hex << seed
                  << std::dec << ' ' << result[index].score << " ("
                  << result[index].moves << " moves, work "
                  << result[index].work << ")\n";
      }
    }));
  }
  for (auto& worker : workers) worker.get();
  return result;
}

double projectedTournamentSeconds(double elapsed, double last_variant_seconds,
                                  int completed_variants) {
  if (completed_variants < 1 || completed_variants > 4 ||
      last_variant_seconds < 0 || elapsed < last_variant_seconds) {
    throw std::invalid_argument("invalid projection inputs");
  }
  if (completed_variants == 4) return elapsed;
  const int current_horizon =
      maximumHorizon(kVariants[completed_variants - 1]);
  double projected = elapsed;
  for (int index = completed_variants; index < 4; ++index) {
    projected += last_variant_seconds *
                 maximumHorizon(kVariants[index]) / current_horizon;
  }
  return projected;
}

struct Options {
  std::string output;
  std::string readme =
      "/tmp/drop7-constructive-horizon-scale-README.md";
  std::string qualification;
  std::string source_sha256;
  int threads = kDefaultThreads;
};

Options parseOptions(int argc, char** argv, int begin) {
  Options result;
  for (int index = begin; index < argc; index += 2) {
    if (index + 1 >= argc) throw std::invalid_argument("missing option value");
    const std::string argument = argv[index];
    if (argument == "--output") {
      result.output = argv[index + 1];
    } else if (argument == "--readme") {
      result.readme = argv[index + 1];
    } else if (argument == "--qualification") {
      result.qualification = argv[index + 1];
    } else if (argument == "--source-sha256") {
      result.source_sha256 = argv[index + 1];
    } else if (argument == "--threads") {
      result.threads = std::stoi(argv[index + 1]);
      if (result.threads < 1 || result.threads > 8) {
        throw std::invalid_argument("threads must be in [1,8]");
      }
    } else {
      throw std::invalid_argument("unknown option " + argument);
    }
  }
  if (result.source_sha256.size() != 64) {
    throw std::invalid_argument("exact 64-character source SHA-256 required");
  }
  return result;
}

void writeSummary(std::ostream& output, const Summary& summary) {
  output << "{\"meanScore\":" << summary.mean_score
         << ",\"meanMoves\":" << summary.mean_moves
         << ",\"bottomQuartileScore\":"
         << summary.bottom_quartile_score
         << ",\"bottomQuartileMoves\":" << summary.bottom_quartile_moves
         << ",\"clearsPerMove\":" << summary.clears_per_move
         << ",\"revealsPerMove\":" << summary.reveals_per_move
         << ",\"wavesPerMove\":" << summary.waves_per_move
         << ",\"logObjective\":" << summary.log_objective
         << ",\"geometricObjective\":" << summary.geometric_objective
         << ",\"naturalTerminals\":" << summary.natural_terminals
         << ",\"capped\":" << summary.capped
         << ",\"maximumChain\":" << summary.maximum_chain
         << ",\"work\":" << summary.work << '}';
}

void writePaired(std::ostream& output, const Paired& paired) {
  output << "{\"scoreWins\":" << paired.score_wins
         << ",\"moveWins\":" << paired.move_wins
         << ",\"jointWins\":" << paired.joint_wins
         << ",\"meanScoreDelta\":" << paired.mean_score_delta
         << ",\"meanMoveDelta\":" << paired.mean_move_delta << '}';
}

void writeGame(std::ostream& output, const GameResult& game) {
  output << "{\"seed\":\"0x" << std::hex << std::setw(8)
         << std::setfill('0') << game.seed << std::dec << std::setfill(' ')
         << "\",\"score\":" << game.score << ",\"moves\":" << game.moves
         << ",\"clears\":" << game.clears
         << ",\"reveals\":" << game.reveals << ",\"waves\":" << game.waves
         << ",\"maximumChain\":" << game.maximum_chain
         << ",\"naturalTerminal\":"
         << (game.natural_terminal ? "true" : "false")
         << ",\"capped\":" << (game.capped ? "true" : "false")
         << ",\"work\":" << game.work << ",\"discHash\":\"0x" << std::hex
         << game.disc_hash << std::dec << "\"}";
}

bool candidateGate(const Summary& candidate, const Summary& baseline,
                   const Paired& paired, int joint_wins) {
  return candidate.mean_score >= kRequiredRatio * baseline.mean_score &&
         candidate.mean_moves >= kRequiredRatio * baseline.mean_moves &&
         candidate.clears_per_move + 1.0e-12 >=
             baseline.clears_per_move &&
         candidate.reveals_per_move + 1.0e-12 >=
             baseline.reveals_per_move &&
         paired.joint_wins >= joint_wins;
}

struct VariantResult {
  Variant variant = Variant::kH7;
  std::vector<GameResult> games;
  Summary summary{};
  Paired versus_h7{};
  double seconds = 0.0;
  bool eligible = false;
};

void writeFitReadme(const Options& options,
                    const std::array<VariantResult, 4>& results,
                    std::optional<Variant> selected, bool passed,
                    double wall_seconds, double projection) {
  std::ofstream output(options.readme);
  if (!output) throw std::runtime_error("cannot write horizon README");
  output << "# Drop7 constructive horizon-scale tournament\n\n"
         << "Only rollout horizon changes. H7 is the exact frozen policy; "
            "H12, H17, and H27 retain its D3 shield, structural target, "
            "weights, rewards, seven scenarios, continuation, sampling, "
            "margin, and tie order.\n\n"
         << "- Phase: fitting\n"
         << "- Seeds: `0x3d6a4000..0x3d6a401f`\n"
         << "- Maximum moves: 1000\n"
         << "- Source SHA-256: `" << options.source_sha256 << "`\n"
         << "- Wall/projected seconds: " << wall_seconds << " / "
         << projection << "\n\n"
         << "| Variant | Mean score | Mean moves | Bottom-Q score | "
            "Bottom-Q moves | Clears | Reveals | Joint wins vs H7 | "
            "Eligible |\n"
         << "|---|---:|---:|---:|---:|---:|---:|---:|---|\n";
  for (const VariantResult& result : results) {
    output << "| " << variantName(result.variant) << " | "
           << result.summary.mean_score << " | " << result.summary.mean_moves
           << " | " << result.summary.bottom_quartile_score << " | "
           << result.summary.bottom_quartile_moves << " | "
           << result.summary.clears_per_move << " | "
           << result.summary.reveals_per_move << " | "
           << (result.variant == Variant::kH7
                   ? 0
                   : result.versus_h7.joint_wins)
           << " | " << (result.eligible ? "yes" : "no") << " |\n";
  }
  output << "\n- Selected: "
         << (selected ? std::string(variantName(*selected)) : "none") << "\n"
         << "- Passed: " << (passed ? "yes" : "no") << "\n\n"
         << "No screen or `0x4d`, `0x7d`, or `0xd7` seed is opened unless "
            "this fitting gate passes.\n";
}

int runFit(const Options& options, std::ostream& output) {
  const Deadline deadline;
  std::array<VariantResult, 4> results{};
  double projected_seconds = 0.0;
  for (int index = 0; index < 4; ++index) {
    const auto started = Clock::now();
    results[index].variant = kVariants[index];
    results[index].games =
        evaluate(kFittingSeedStart, kFittingGames, kVariants[index],
                 options.threads, deadline, false);
    results[index].seconds =
        std::chrono::duration<double>(Clock::now() - started).count();
    results[index].summary = summarize(results[index].games);
    projected_seconds = projectedTournamentSeconds(
        deadline.seconds(), results[index].seconds, index + 1);
    std::cerr << "projection after " << variantName(kVariants[index]) << ' '
              << projected_seconds << " seconds\n";
    if (projected_seconds > kWallLimitSeconds) {
      throw std::runtime_error(
          "measured fitting projection exceeded 45 minute cap");
    }
    deadline.check();
    enforceRssLimit();
  }
  const VariantResult& baseline = results[0];
  std::optional<Variant> selected;
  double selected_objective = -std::numeric_limits<double>::infinity();
  for (int index = 1; index < 4; ++index) {
    results[index].versus_h7 =
        pair(results[index].games, baseline.games);
    results[index].eligible =
        candidateGate(results[index].summary, baseline.summary,
                      results[index].versus_h7, kFitJointWins);
    if (results[index].eligible &&
        results[index].summary.log_objective > selected_objective) {
      selected = results[index].variant;
      selected_objective = results[index].summary.log_objective;
    }
  }
  const bool resource_gate = deadline.seconds() <= kWallLimitSeconds &&
                             projected_seconds <= kWallLimitSeconds &&
                             peakRssBytes() <= kRssLimitBytes;
  const bool passed = selected.has_value() && resource_gate;
  const std::string output_path =
      options.output.empty()
          ? "/tmp/drop7-constructive-horizon-scale-fit.json"
          : options.output;
  std::ofstream artifact(output_path);
  if (!artifact) throw std::runtime_error("cannot write horizon fit artifact");
  artifact << std::fixed << std::setprecision(9)
           << "{\n  \"format\":\"drop7-constructive-horizon-scale-v1\","
           << "\n  \"phase\":\"fitting\",\n  \"sourceSha256\":\""
           << options.source_sha256
           << "\",\n  \"publicOnly\":true,\n  \"causal\":true,"
           << "\n  \"isolatedVariable\":\"rolloutHorizon\","
           << "\n  \"horizons\":{\"H7\":\"min(remaining+5,7)\","
              "\"H12\":\"min(remaining+10,12)\","
              "\"H17\":\"min(remaining+15,17)\","
              "\"H27\":\"min(remaining+25,27)\"},"
           << "\n  \"seedBank\":{\"start\":\"0x3d6a4000\","
              "\"endExclusive\":\"0x3d6a4020\",\"games\":32,"
              "\"maximumMoves\":1000},"
           << "\n  \"selectionObjective\":\"equal mean of log meanScore, "
              "log meanMoves, log bottomQuartileScore, log "
              "bottomQuartileMoves\","
           << "\n  \"variants\":[";
  for (int index = 0; index < 4; ++index) {
    if (index) artifact << ',';
    const VariantResult& result = results[index];
    artifact << "{\"name\":\"" << variantName(result.variant)
             << "\",\"seconds\":" << result.seconds
             << ",\"summary\":";
    writeSummary(artifact, result.summary);
    artifact << ",\"versusH7\":";
    writePaired(artifact, result.versus_h7);
    artifact << ",\"eligible\":"
             << (result.eligible ? "true" : "false") << ",\"games\":[";
    for (std::size_t game = 0; game < result.games.size(); ++game) {
      if (game) artifact << ',';
      writeGame(artifact, result.games[game]);
    }
    artifact << "]}";
  }
  artifact << "],\n  \"gate\":{\"scoreRatio\":1.10,\"moveRatio\":1.10,"
              "\"clearNonregression\":true,"
              "\"revealNonregression\":true,\"jointWins\":20},"
           << "\n  \"selectedVariant\":"
           << (selected ? "\"" + std::string(variantName(*selected)) + "\""
                        : "null")
           << ",\n  \"resourceGate\":"
           << (resource_gate ? "true" : "false")
           << ",\n  \"projectedSeconds\":" << projected_seconds
           << ",\n  \"passed\":" << (passed ? "true" : "false")
           << ",\n  \"wallSeconds\":" << deadline.seconds()
           << ",\n  \"peakRssBytes\":" << peakRssBytes() << "\n}\n";
  writeFitReadme(options, results, selected, passed, deadline.seconds(),
                 projected_seconds);
  output << std::fixed << std::setprecision(3)
         << "CONSTRUCTIVE_HORIZON_SCALE_FIT {\"H7Score\":"
         << results[0].summary.mean_score << ",\"H7Moves\":"
         << results[0].summary.mean_moves << ",\"H12Score\":"
         << results[1].summary.mean_score << ",\"H12Moves\":"
         << results[1].summary.mean_moves << ",\"H17Score\":"
         << results[2].summary.mean_score << ",\"H17Moves\":"
         << results[2].summary.mean_moves << ",\"H27Score\":"
         << results[3].summary.mean_score << ",\"H27Moves\":"
         << results[3].summary.mean_moves << ",\"selected\":"
         << (selected ? "\"" + std::string(variantName(*selected)) + "\""
                      : "null")
         << ",\"passed\":" << (passed ? "true" : "false")
         << ",\"wallSeconds\":" << deadline.seconds()
         << ",\"peakRssBytes\":" << peakRssBytes()
         << ",\"artifact\":\"" << output_path << "\"}\n";
  return passed ? EXIT_SUCCESS : 2;
}

Variant qualifiedVariant(const Options& options) {
  if (options.qualification.empty()) {
    throw std::invalid_argument("screen requires fitting qualification");
  }
  std::ifstream input(options.qualification);
  if (!input) throw std::invalid_argument("cannot open fitting qualification");
  const std::string contents((std::istreambuf_iterator<char>(input)),
                             std::istreambuf_iterator<char>());
  if (contents.find("\"phase\":\"fitting\"") == std::string::npos ||
      contents.find("\"passed\":true") == std::string::npos ||
      contents.find("\"sourceSha256\":\"" + options.source_sha256 +
                    "\"") == std::string::npos) {
    throw std::invalid_argument("fitting qualification did not pass/match");
  }
  const std::string tag = "\"selectedVariant\":\"";
  const auto begin = contents.find(tag);
  if (begin == std::string::npos) {
    throw std::invalid_argument("qualification lacks selected variant");
  }
  const auto value_begin = begin + tag.size();
  const auto value_end = contents.find('"', value_begin);
  if (value_end == std::string::npos) {
    throw std::invalid_argument("malformed selected variant");
  }
  const Variant result =
      parseVariant(contents.substr(value_begin, value_end - value_begin));
  if (result == Variant::kH7) {
    throw std::invalid_argument("H7 cannot qualify as its own challenger");
  }
  return result;
}

void writeScreenReadme(const Options& options, Variant selected,
                       const Summary& candidate, const Summary& baseline,
                       const Paired& paired, bool passed, double wall_seconds) {
  std::ofstream output(options.readme);
  if (!output) throw std::runtime_error("cannot write horizon README");
  output << "# Drop7 constructive horizon-scale screen\n\n"
         << "The fitting-qualified horizon is compared once against exact "
            "frozen H7. Only rollout length differs.\n\n"
         << "- Selected: " << variantName(selected) << "\n"
         << "- Seeds: `0x3d6a5000..0x3d6a503f`\n"
         << "- Maximum moves: 1000\n"
         << "- Source SHA-256: `" << options.source_sha256 << "`\n"
         << "- Candidate mean score/moves: " << candidate.mean_score << " / "
         << candidate.mean_moves << "\n"
         << "- H7 mean score/moves: " << baseline.mean_score << " / "
         << baseline.mean_moves << "\n"
         << "- Candidate clear/reveal flow: " << candidate.clears_per_move
         << " / " << candidate.reveals_per_move << "\n"
         << "- H7 clear/reveal flow: " << baseline.clears_per_move << " / "
         << baseline.reveals_per_move << "\n"
         << "- Paired joint wins: " << paired.joint_wins << "\n"
         << "- Candidate natural/censored: " << candidate.natural_terminals
         << " / " << candidate.capped << "\n"
         << "- H7 natural/censored: " << baseline.natural_terminals << " / "
         << baseline.capped << "\n"
         << "- Wall seconds: " << wall_seconds << "\n"
         << "- Passed: " << (passed ? "yes" : "no") << "\n";
}

int runScreen(const Options& options, std::ostream& output) {
  const Variant selected = qualifiedVariant(options);
  const Deadline deadline;
  const auto candidate = evaluate(kScreenSeedStart, kScreenGames, selected,
                                  options.threads, deadline, true);
  const auto baseline = evaluate(kScreenSeedStart, kScreenGames, Variant::kH7,
                                 options.threads, deadline, true);
  const Summary candidate_summary = summarize(candidate);
  const Summary baseline_summary = summarize(baseline);
  const Paired paired = pair(candidate, baseline);
  const bool result_gate =
      candidateGate(candidate_summary, baseline_summary, paired,
                    kScreenJointWins);
  const bool resource_gate = deadline.seconds() <= kWallLimitSeconds &&
                             peakRssBytes() <= kRssLimitBytes;
  const bool passed = result_gate && resource_gate;
  const std::string output_path =
      options.output.empty()
          ? "/tmp/drop7-constructive-horizon-scale-screen.json"
          : options.output;
  std::ofstream artifact(output_path);
  if (!artifact) {
    throw std::runtime_error("cannot write horizon screen artifact");
  }
  artifact << std::fixed << std::setprecision(9)
           << "{\n  \"format\":\"drop7-constructive-horizon-scale-v1\","
           << "\n  \"phase\":\"screen\",\n  \"sourceSha256\":\""
           << options.source_sha256 << "\",\n  \"selectedVariant\":\""
           << variantName(selected)
           << "\",\n  \"publicOnly\":true,\n  \"causal\":true,"
           << "\n  \"seedBank\":{\"start\":\"0x3d6a5000\","
              "\"endExclusive\":\"0x3d6a5040\",\"games\":64,"
              "\"maximumMoves\":1000},"
           << "\n  \"candidate\":";
  writeSummary(artifact, candidate_summary);
  artifact << ",\n  \"H7\":";
  writeSummary(artifact, baseline_summary);
  artifact << ",\n  \"paired\":";
  writePaired(artifact, paired);
  artifact << ",\n  \"gate\":{\"scoreRatio\":1.10,\"moveRatio\":1.10,"
              "\"clearNonregression\":true,"
              "\"revealNonregression\":true,\"jointWins\":40},"
           << "\n  \"resultGate\":" << (result_gate ? "true" : "false")
           << ",\n  \"resourceGate\":"
           << (resource_gate ? "true" : "false")
           << ",\n  \"passed\":" << (passed ? "true" : "false")
           << ",\n  \"wallSeconds\":" << deadline.seconds()
           << ",\n  \"peakRssBytes\":" << peakRssBytes()
           << ",\n  \"candidateGames\":[";
  for (std::size_t index = 0; index < candidate.size(); ++index) {
    if (index) artifact << ',';
    writeGame(artifact, candidate[index]);
  }
  artifact << "],\n  \"H7Games\":[";
  for (std::size_t index = 0; index < baseline.size(); ++index) {
    if (index) artifact << ',';
    writeGame(artifact, baseline[index]);
  }
  artifact << "]\n}\n";
  writeScreenReadme(options, selected, candidate_summary, baseline_summary,
                    paired, passed, deadline.seconds());
  output << std::fixed << std::setprecision(3)
         << "CONSTRUCTIVE_HORIZON_SCALE_SCREEN {\"selected\":\""
         << variantName(selected) << "\",\"candidateScore\":"
         << candidate_summary.mean_score << ",\"candidateMoves\":"
         << candidate_summary.mean_moves << ",\"H7Score\":"
         << baseline_summary.mean_score << ",\"H7Moves\":"
         << baseline_summary.mean_moves << ",\"jointWins\":"
         << paired.joint_wins << ",\"passed\":"
         << (passed ? "true" : "false") << ",\"wallSeconds\":"
         << deadline.seconds() << ",\"peakRssBytes\":" << peakRssBytes()
         << ",\"artifact\":\"" << output_path << "\"}\n";
  return passed ? EXIT_SUCCESS : 2;
}

void expect(bool condition, std::string_view message) {
  if (!condition) throw std::runtime_error(std::string(message));
}

template <typename Function>
bool throwsInvalid(Function&& function) {
  try {
    function();
  } catch (const std::invalid_argument&) {
    return true;
  }
  return false;
}

bool selfTest(std::ostream& output) {
  expect(kLevelBonus == 17'000 && frozen::kChanceSamples == 7 &&
             frozen::kTacticalDepth == 3 &&
             frozen::kTacticalShortlist == 2 &&
             frozen::kTacticalNearTie == 2'500.0,
         "frozen controller constants changed");
  PublicState fixture;
  fixture.board.fill(kEmpty);
  fixture.board[indexOf(6, 0)] = kSolid;
  fixture.board[indexOf(5, 0)] = 6;
  fixture.board[indexOf(6, 1)] = kCracked;
  fixture.board[indexOf(6, 2)] = 5;
  fixture.board[indexOf(5, 2)] = 4;
  fixture.board[indexOf(6, 3)] = kSolid;
  fixture.board[indexOf(6, 4)] = 7;
  fixture.next_disc = 3;
  fixture.moves_remaining = 1;
  expect(horizonFor(fixture, Variant::kH7) == 6 &&
             horizonFor(fixture, Variant::kH12) == 11 &&
             horizonFor(fixture, Variant::kH17) == 16 &&
             horizonFor(fixture, Variant::kH27) == 26,
         "low-phase horizon formula failed");
  fixture.moves_remaining = 5;
  expect(horizonFor(fixture, Variant::kH7) == 7 &&
             horizonFor(fixture, Variant::kH12) == 12 &&
             horizonFor(fixture, Variant::kH17) == 17 &&
             horizonFor(fixture, Variant::kH27) == 27,
         "maximum horizon formula failed");

  const Decision frozen_h7 = frozen::chooseAction(fixture);
  const Decision scaled_h7 = chooseAction(fixture, Variant::kH7);
  const Decision generic_h7 = chooseActionCanonical(fixture, Variant::kH7);
  expect(scaled_h7 == frozen_h7 && generic_h7 ==
                                      frozen::chooseActionCanonical(fixture),
         "exact H7 action/value/work parity failed");
  const Decision h27 = chooseAction(fixture, Variant::kH27);
  const Decision h27_repeat = chooseAction(fixture, Variant::kH27);
  const Decision h27_reflected =
      chooseAction(frozen::mirror(fixture), Variant::kH27);
  expect(h27 == h27_repeat && isLegal(fixture.board, h27.action) &&
             h27.horizon == 27 && h27.shortlist >= 1 && h27.shortlist <= 2,
         "H27 determinism/legality failed");
  expect(h27_reflected.action == kBoardSize - 1 - h27.action &&
             h27_reflected.tactical_action ==
                 kBoardSize - 1 - h27.tactical_action &&
             h27_reflected.work == h27.work &&
             h27_reflected.shortlist == h27.shortlist,
         "H27 reflection failed");
  for (int column = 0; column < kBoardSize; ++column) {
    expect(h27.values[column] ==
               h27_reflected.values[kBoardSize - 1 - column],
           "H27 values failed reflection");
  }
  State metadata = frozen::materialize(fixture);
  metadata.score = 9'999'999;
  metadata.level = 777;
  metadata.moves_played = 888;
  expect(frozen::publicState(metadata) == fixture &&
             chooseAction(frozen::publicState(metadata), Variant::kH27) == h27,
         "horizon policy used hidden metadata");
  PublicState terminal = fixture;
  terminal.terminal = true;
  expect(chooseAction(terminal, Variant::kH7).action == -1 &&
             chooseAction(terminal, Variant::kH27).action == -1,
         "terminal horizon policy selected an action");
  expect(projectedTournamentSeconds(10.0, 10.0, 1) == 90.0 &&
             projectedTournamentSeconds(40.0, 10.0, 4) == 40.0 &&
             throwsInvalid([] {
               (void)projectedTournamentSeconds(1.0, 2.0, 1);
             }),
         "measured projection formula failed");
  expect(allowedFittingSeed(kFittingSeedStart) &&
             allowedFittingSeed(kFittingSeedEndExclusive - 1u) &&
             !allowedFittingSeed(kFittingSeedStart - 1u) &&
             !allowedFittingSeed(kFittingSeedEndExclusive) &&
             allowedScreenSeed(kScreenSeedStart) &&
             allowedScreenSeed(kScreenSeedEndExclusive - 1u) &&
             !allowedScreenSeed(kScreenSeedStart - 1u) &&
             !allowedScreenSeed(kScreenSeedEndExclusive) &&
             throwsInvalid([] { requireSeed(0x4d6a'4000u, false); }) &&
             throwsInvalid([] { requireSeed(0x7d6a'4000u, false); }) &&
             throwsInvalid([] { requireSeed(0xd76a'4000u, false); }) &&
             throwsInvalid([] { requireSeed(0x4d6a'5000u, true); }) &&
             throwsInvalid([] { requireSeed(0x7d6a'5000u, true); }) &&
             throwsInvalid([] { requireSeed(0xd76a'5000u, true); }),
         "horizon seed guards failed");
  enforceRssLimit();
  output << "CONSTRUCTIVE_HORIZON_SCALE_SELF_TEST {\"passed\":true,"
         << "\"publicOnly\":true,\"metadataBlind\":true,"
         << "\"deterministic\":true,\"reflection\":true,"
         << "\"legal\":true,\"exactH7Parity\":true,"
         << "\"horizons\":[7,12,17,27],\"projectionGuard\":true,"
         << "\"seedGuards\":true,\"H7Work\":" << scaled_h7.work
         << ",\"H27Work\":" << h27.work
         << ",\"peakRssBytes\":" << peakRssBytes() << "}\n";
  return true;
}

}  // namespace drop7::constructive_horizon_scale

int main(int argc, char** argv) {
  try {
    using namespace drop7::constructive_horizon_scale;
    if (argc >= 2 && std::string_view(argv[1]) == "--self-test") {
      return selfTest(std::cout) ? EXIT_SUCCESS : EXIT_FAILURE;
    }
    if (argc >= 2 && std::string_view(argv[1]) == "--fit") {
      return runFit(parseOptions(argc, argv, 2), std::cout);
    }
    if (argc >= 2 && std::string_view(argv[1]) == "--screen") {
      return runScreen(parseOptions(argc, argv, 2), std::cout);
    }
    std::cerr << "usage: drop7_constructive_horizon_scale --self-test | "
                 "--fit --source-sha256 HASH [--output PATH] [--readme PATH] "
                 "[--threads N] | --screen --source-sha256 HASH "
                 "--qualification FIT_JSON [--output PATH] [--readme PATH] "
                 "[--threads N]\n";
    return 2;
  } catch (const std::exception& error) {
    std::cerr << "drop7_constructive_horizon_scale: " << error.what() << '\n';
    return EXIT_FAILURE;
  }
}