Drop7 Research
approaches/constructive-reservoir/viability-controller/viability-reservoir-controller.cppC++1,728 lines · 64.0 KB
#define DROP7_FAIR_ONLY_HORIZON_LIBRARY
#define DROP7_FAIR_ONLY_DEPTH4_LIBRARY
#define DROP7_FAIR_ONLY_DEPTH4_NO_MAIN
#include "../../fair-expectimax/reference/fair-only-depth4.cpp"
#undef DROP7_FAIR_ONLY_DEPTH4_NO_MAIN
#undef DROP7_FAIR_ONLY_DEPTH4_LIBRARY
#undef DROP7_FAIR_ONLY_HORIZON_LIBRARY

#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 <numeric>
#include <stdexcept>
#include <string>
#include <string_view>
#include <sys/resource.h>
#include <type_traits>
#include <utility>
#include <vector>

// Implements a non-parametric viability-reservoir controller.  The runtime
// decision accepts only the public board, next disc, rise phase, and terminal
// flag.  It constructs an exact conservative cascade
// certificate for all 7 x 7 possible next trigger keys, chooses a public-state
// option, applies a terminal/viability shield, and ranks the surviving actions
// lexicographically.  There is no fitted scalar leaf and no gameplay history.
namespace drop7::viability_reservoir_controller {

namespace fair = drop7::fair_only_horizon;
namespace d4 = drop7::fair_only_depth4;
namespace detail = drop7::cfpi::detail;

using Clock = std::chrono::steady_clock;

constexpr std::uint32_t kStageASeedStart = 0x3d65'c000u;
constexpr std::uint32_t kStageASeedEndExclusive = 0x3d65'c020u;
constexpr int kStageAGames = 32;
constexpr int kMaximumMoves = 1'000;
constexpr int kSuccessorSamples = kBoardSize;
constexpr std::uint32_t kPolicySeed = 0x5652'4331u;  // "VRC1"
constexpr double kWallLimitSeconds = 30.0 * 60.0;
constexpr std::uint64_t kRssLimitBytes = 256ull * 1024ull * 1024ull;
constexpr std::uint8_t kInertReveal = 10;
constexpr int kRankFields = 18;

constexpr double kGateMeanScore = 700'000.0;
constexpr double kGateMeanMoves = 200.0;
constexpr double kGateClearsPerMove = 2.20;
constexpr double kGateRevealsPerMove = 1.20;
constexpr double kGateBottomQuartileMoves = 120.0;
constexpr int kGateJointWins = 24;

constexpr std::array<int, kBoardSize> kColumnOrder{{3, 2, 4, 1, 5, 0, 6}};

static_assert(kLevelBonus == 17'000);
static_assert(kMovesPerLevel == 5);
static_assert(kSuccessorSamples == 7);
static_assert(kStageASeedEndExclusive - kStageASeedStart == kStageAGames);
static_assert((kStageASeedStart >> 16u) == 0x3d65u);
static_assert((kStageASeedEndExclusive - 1u) >> 16u == 0x3d65u);
static_assert((kStageASeedStart >> 24u) != 0x4du);
static_assert((kStageASeedStart >> 24u) != 0x7du);
static_assert((kStageASeedStart >> 24u) != 0xd7u);

struct PublicState {
  Board board{};
  std::uint8_t next_disc = 1;
  std::uint8_t moves_remaining = kMovesPerLevel;
  bool terminal = false;

  bool operator==(const PublicState&) const = default;
};

PublicState publicState(const State& source) {
  if (source.next_disc < 1 || source.next_disc > kBoardSize ||
      source.moves_remaining < 0 || source.moves_remaining > kMovesPerLevel ||
      (!source.game_over && source.moves_remaining < 1)) {
    throw std::invalid_argument("invalid public viability-reservoir state");
  }
  for (const std::uint8_t cell : source.board) {
    if (cell > kCracked) throw std::invalid_argument("invalid board token");
  }
  return {source.board, source.next_disc,
          static_cast<std::uint8_t>(source.moves_remaining), source.game_over};
}

State materialize(const PublicState& source) {
  State result;
  result.board = source.board;
  result.next_disc = source.next_disc;
  result.moves_remaining = source.moves_remaining;
  result.game_over = source.terminal;
  return result;
}

PublicState mirror(const PublicState& source) {
  PublicState result = source;
  result.board = detail::mirrorBoard(source.board);
  return result;
}

PublicState canonicalState(const PublicState& source, bool& mirrored) {
  mirrored = detail::mirroredRepresentationIsSmaller(source.board);
  return mirrored ? mirror(source) : source;
}

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(
        "viability reservoir exceeded the 256 MiB RSS cap");
  }
}

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

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

  void check() const {
    if (elapsedSeconds() > kWallLimitSeconds) {
      throw std::runtime_error(
          "viability reservoir exceeded the 30 minute wall cap");
    }
  }
};

std::array<int, kBoardSize> columnHeights(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;
}

enum class EdgeKind : std::uint8_t {
  kLineNeighbor,
  kVerticalSupport,
  kCoverFrontier,
};

struct CausalEdge {
  std::uint8_t from = 0;
  std::uint8_t to = 0;
  EdgeKind kind = EdgeKind::kLineNeighbor;

  bool operator==(const CausalEdge&) const = default;
};

struct DiscNode {
  std::uint8_t cell = 0;
  std::uint8_t value = 0;
  std::uint8_t horizontal_length = 0;
  std::uint8_t vertical_length = 0;
  std::int8_t horizontal_deficit = 0;
  std::int8_t vertical_deficit = 0;
  std::int8_t horizontal_build_cost = -1;
  std::int8_t support_cell = -1;
  std::uint8_t adjacent_covers = 0;

  bool operator==(const DiscNode&) const = default;
};

struct GraphStats {
  int occupied = 0;
  int maximum_height = 0;
  int top_slack = kBoardSize;
  int open_columns = kBoardSize;
  int solid_cells = 0;
  int cracked_cells = 0;
  int numbered_cells = 0;
  int inert_cells = 0;
  int cover_altitude_debt = 0;
  int edge_cover_debt = 0;
  int frontier_access = 0;
  int stored_mass = 0;
  int release_ready = 0;
  int same_target_pairs = 0;
  int adjacent_ones = 0;
  int triple_twos = 0;
  int dead_low_numbers = 0;
  int capped_low_columns = 0;
  int clog_debt = 0;
  int line_edges = 0;
  int support_edges = 0;
  int frontier_edges = 0;

  bool operator==(const GraphStats&) const = default;
};

struct CertificateGraph {
  std::array<DiscNode, kCellCount> nodes{};
  int node_count = 0;
  std::array<CausalEdge, 512> edges{};
  int edge_count = 0;
  GraphStats stats{};

  bool operator==(const CertificateGraph&) const = default;
};

void addEdge(CertificateGraph& graph, int from, int to, EdgeKind kind) {
  if (graph.edge_count >= static_cast<int>(graph.edges.size())) {
    throw std::runtime_error("causal certificate edge capacity exceeded");
  }
  graph.edges[graph.edge_count++] = {
      static_cast<std::uint8_t>(from), static_cast<std::uint8_t>(to), kind};
  if (kind == EdgeKind::kLineNeighbor) ++graph.stats.line_edges;
  if (kind == EdgeKind::kVerticalSupport) ++graph.stats.support_edges;
  if (kind == EdgeKind::kCoverFrontier) ++graph.stats.frontier_edges;
}

int horizontalBuildCost(const Board& board,
                        const std::array<int, kBoardSize>& heights, int row,
                        int column, int value) {
  const int length = lineLength(board, row, column, false);
  if (value <= length) return -1;
  int start = column;
  int end = column;
  while (start > 0 && board[indexOf(row, start - 1)] != kEmpty) --start;
  while (end + 1 < kBoardSize &&
         board[indexOf(row, end + 1)] != kEmpty) {
    ++end;
  }
  const int additions = value - length;
  const int target_elevation = kBoardSize - row;
  int best = std::numeric_limits<int>::max();
  for (int left = 0; left <= additions; ++left) {
    const int right = additions - left;
    if (start - left < 0 || end + right >= kBoardSize) continue;
    int cost = 0;
    bool valid = true;
    for (int offset = 1; offset <= left; ++offset) {
      const int next_column = start - offset;
      if (board[indexOf(row, next_column)] != kEmpty ||
          heights[next_column] >= target_elevation) {
        valid = false;
        break;
      }
      cost += target_elevation - heights[next_column];
    }
    for (int offset = 1; valid && offset <= right; ++offset) {
      const int next_column = end + offset;
      if (board[indexOf(row, next_column)] != kEmpty ||
          heights[next_column] >= target_elevation) {
        valid = false;
        break;
      }
      cost += target_elevation - heights[next_column];
    }
    if (valid) best = std::min(best, cost);
  }
  return best <= 14 ? best : -1;
}

CertificateGraph buildCertificateGraph(const Board& board) {
  CertificateGraph graph;
  const auto heights = columnHeights(board);
  std::array<std::array<int, kBoardSize + 1>, kBoardSize> vertical_targets{};

  for (int column = 0; column < kBoardSize; ++column) {
    graph.stats.occupied += heights[column];
    graph.stats.maximum_height =
        std::max(graph.stats.maximum_height, heights[column]);
    graph.stats.open_columns -= heights[column] == kBoardSize;
  }
  graph.stats.top_slack = kBoardSize - graph.stats.maximum_height;

  constexpr std::array<std::array<int, 2>, 4> directions{{
      {{-1, 0}}, {{1, 0}}, {{0, -1}}, {{0, 1}},
  }};
  for (int row = 0; row < kBoardSize; ++row) {
    const int elevation = kBoardSize - row;
    for (int column = 0; column < kBoardSize; ++column) {
      const int cell_index = indexOf(row, column);
      const std::uint8_t cell = board[cell_index];
      if (cell == kEmpty) continue;
      if (cell == kSolid || cell == kCracked) {
        const int type_weight = cell == kSolid ? 3 : 2;
        graph.stats.solid_cells += cell == kSolid;
        graph.stats.cracked_cells += cell == kCracked;
        graph.stats.cover_altitude_debt +=
            type_weight * elevation * elevation;
        if (column == 0 || column == kBoardSize - 1) {
          graph.stats.edge_cover_debt += type_weight * elevation * elevation;
        }
        for (const int neighbor : {column - 1, column + 1}) {
          if (neighbor < 0 || neighbor >= kBoardSize ||
              heights[neighbor] >= elevation) {
            continue;
          }
          const int distance = elevation - heights[neighbor];
          graph.stats.frontier_access +=
              type_weight * std::max(0, kBoardSize + 1 - distance);
        }
        continue;
      }
      if (cell == kInertReveal) {
        ++graph.stats.inert_cells;
        continue;
      }
      if (!isNumbered(cell)) {
        throw std::invalid_argument("invalid certificate board token");
      }

      ++graph.stats.numbered_cells;
      DiscNode node;
      node.cell = static_cast<std::uint8_t>(cell_index);
      node.value = cell;
      node.horizontal_length = static_cast<std::uint8_t>(
          lineLength(board, row, column, false));
      node.vertical_length = static_cast<std::uint8_t>(
          lineLength(board, row, column, true));
      node.horizontal_deficit = static_cast<std::int8_t>(
          static_cast<int>(cell) - node.horizontal_length);
      node.vertical_deficit = static_cast<std::int8_t>(
          static_cast<int>(cell) - node.vertical_length);
      node.horizontal_build_cost = static_cast<std::int8_t>(
          horizontalBuildCost(board, heights, row, column, cell));
      node.support_cell = static_cast<std::int8_t>(
          row + 1 < kBoardSize ? indexOf(row + 1, column) : -1);

      for (const auto& direction : directions) {
        const int next_row = row + direction[0];
        const int next_column = column + direction[1];
        if (!inside(next_row, next_column)) continue;
        const int next_index = indexOf(next_row, next_column);
        const std::uint8_t neighbor = board[next_index];
        if (neighbor == kSolid || neighbor == kCracked) {
          ++node.adjacent_covers;
          addEdge(graph, cell_index, next_index, EdgeKind::kCoverFrontier);
        } else if (neighbor != kEmpty) {
          const EdgeKind kind = direction[0] != 0
                                    ? EdgeKind::kVerticalSupport
                                    : EdgeKind::kLineNeighbor;
          addEdge(graph, cell_index, next_index, kind);
        }
      }

      const int value = cell;
      if (node.vertical_deficit > 0) {
        const int gap = node.vertical_deficit;
        const int high_multiplier = value >= 5 ? 2 : 1;
        graph.stats.stored_mass +=
            high_multiplier * (value + 1) * std::max(0, 8 - gap);
        graph.stats.release_ready += gap == 1;
        ++vertical_targets[column][value];
      }
      if (node.horizontal_build_cost > 0) {
        const int high_multiplier = value >= 5 ? 2 : 1;
        graph.stats.stored_mass +=
            high_multiplier * (value + 1) *
            std::max(0, 8 - node.horizontal_build_cost);
        graph.stats.release_ready += node.horizontal_build_cost == 1;
      }
      graph.nodes[graph.node_count++] = node;
    }
  }

  for (int column = 0; column < kBoardSize; ++column) {
    for (int value = 1; value <= kBoardSize; ++value) {
      const int count = vertical_targets[column][value];
      if (count >= 2) {
        const int pairs = count * (count - 1) / 2;
        graph.stats.same_target_pairs += pairs;
        graph.stats.stored_mass += pairs * value * 6;
      }
    }
  }

  for (int row = 0; row < kBoardSize; ++row) {
    for (int column = 0; column < kBoardSize; ++column) {
      const std::uint8_t cell = board[indexOf(row, column)];
      if (cell == 1) {
        if (column + 1 < kBoardSize &&
            board[indexOf(row, column + 1)] == 1) {
          ++graph.stats.adjacent_ones;
        }
        if (row + 1 < kBoardSize && board[indexOf(row + 1, column)] == 1) {
          ++graph.stats.adjacent_ones;
        }
      }
      if (cell == 1 || cell == 2) {
        const int horizontal = lineLength(board, row, column, false);
        const int vertical = lineLength(board, row, column, true);
        graph.stats.dead_low_numbers +=
            horizontal > cell && vertical > cell;
      }
    }
  }

  const auto countTwoRun = [&](int start_row, int start_column, int row_step,
                               int column_step) {
    int run = 0;
    int triples = 0;
    for (int offset = 0; offset < kBoardSize; ++offset) {
      const int row = start_row + row_step * offset;
      const int column = start_column + column_step * offset;
      if (board[indexOf(row, column)] == 2) {
        ++run;
      } else {
        triples += std::max(0, run - 2);
        run = 0;
      }
    }
    return triples + std::max(0, run - 2);
  };
  for (int row = 0; row < kBoardSize; ++row) {
    graph.stats.triple_twos += countTwoRun(row, 0, 0, 1);
  }
  for (int column = 0; column < kBoardSize; ++column) {
    graph.stats.triple_twos += countTwoRun(0, column, 1, 0);
    if (heights[column] == 0) continue;
    const std::uint8_t cap =
        board[indexOf(kBoardSize - heights[column], column)];
    graph.stats.capped_low_columns +=
        heights[column] >= 4 && (cap == 1 || cap == 2);
  }
  graph.stats.clog_debt = 8 * graph.stats.adjacent_ones +
                          12 * graph.stats.triple_twos +
                          4 * graph.stats.dead_low_numbers +
                          5 * graph.stats.capped_low_columns;
  return graph;
}

struct ConservativeResult {
  Board board{};
  bool played = false;
  bool terminal = false;
  bool level_advanced = false;
  int moves_remaining = 0;
  int clears = 0;
  int reveals = 0;
  int cracks = 0;
  int cover_hits = 0;
  int waves = 0;
  int maximum_depth = 0;

  bool operator==(const ConservativeResult&) const = default;
};

void resolveConservatively(Board& board, int starting_depth,
                           ConservativeResult& result) {
  constexpr std::array<std::array<int, 2>, 4> directions{{
      {{-1, 0}}, {{1, 0}}, {{0, -1}}, {{0, 1}},
  }};
  for (int depth = starting_depth;; ++depth) {
    int popper_count = 0;
    const auto poppers = findPoppers(board, popper_count);
    if (popper_count == 0) return;
    std::array<bool, kCellCount> popping{};
    Board cleared = board;
    for (int offset = 0; offset < popper_count; ++offset) {
      const int popper = poppers[offset];
      popping[popper] = true;
      cleared[popper] = kEmpty;
    }
    for (int row = 0; row < kBoardSize; ++row) {
      for (int column = 0; column < kBoardSize; ++column) {
        const int cell_index = indexOf(row, column);
        const std::uint8_t cell = board[cell_index];
        if (cell != kSolid && cell != kCracked) continue;
        int hits = 0;
        for (const auto& direction : directions) {
          const int neighbor_row = row + direction[0];
          const int neighbor_column = column + direction[1];
          if (inside(neighbor_row, neighbor_column) &&
              popping[indexOf(neighbor_row, neighbor_column)]) {
            ++hits;
          }
        }
        result.cover_hits += hits;
        if (hits == 0) continue;
        const int required = cell == kSolid ? 2 : 1;
        if (hits >= required) {
          cleared[cell_index] = kInertReveal;
          ++result.reveals;
        } else if (cell == kSolid) {
          cleared[cell_index] = kCracked;
          ++result.cracks;
        }
      }
    }
    result.clears += popper_count;
    ++result.waves;
    result.maximum_depth = depth;
    board = applyGravity(cleared);
  }
}

ConservativeResult conservativePlay(const PublicState& state, int column,
                                    std::uint8_t disc) {
  ConservativeResult result;
  result.board = state.board;
  if (state.terminal || disc < 1 || disc > kBoardSize ||
      !placeDisc(result.board, column, disc)) {
    return result;
  }
  result.played = true;
  resolveConservatively(result.board, 1, result);
  result.moves_remaining = static_cast<int>(state.moves_remaining) - 1;
  if (result.moves_remaining == 0) {
    Board raised{};
    if (!raiseCoveredRow(result.board, raised)) {
      result.terminal = true;
    } else {
      result.level_advanced = true;
      result.moves_remaining = kMovesPerLevel;
      result.board = raised;
      resolveConservatively(result.board, result.maximum_depth + 1, result);
    }
  }
  int legal_count = 0;
  legalColumns(result.board, legal_count);
  if (!result.terminal && legal_count == 0) result.terminal = true;
  return result;
}

struct KeyCertificate {
  bool legal = false;
  bool survives = false;
  int clears = 0;
  int reveals = 0;
  int cracks = 0;
  int waves = 0;
  int maximum_depth = 0;
  int top_slack = 0;
  int build_gain = 0;
  int frontier_gain = 0;
  int clog_improvement = 0;
  int release_strength = 0;
  int quality = 0;
  bool productive = false;

  bool operator==(const KeyCertificate&) const = default;
};

struct TriggerSummary {
  int worst_safe_columns = kBoardSize;
  int worst_productive_columns = kBoardSize;
  int worst_best_release = std::numeric_limits<int>::max();
  int worst_best_quality = std::numeric_limits<int>::max();
  int total_safe_columns = 0;
  int total_productive_columns = 0;
  int total_best_release = 0;
  int total_best_quality = 0;
  int strong_keys = 0;

  bool operator==(const TriggerSummary&) const = default;
};

struct TriggerMatrix {
  std::array<std::array<KeyCertificate, kBoardSize>, kBoardSize> keys{};
  TriggerSummary summary{};

  bool operator==(const TriggerMatrix&) const = default;
};

TriggerMatrix buildTriggerMatrix(const PublicState& state,
                                 std::uint64_t& simulations) {
  TriggerMatrix matrix;
  if (state.terminal) {
    matrix.summary.worst_safe_columns = 0;
    matrix.summary.worst_productive_columns = 0;
    matrix.summary.worst_best_release = 0;
    matrix.summary.worst_best_quality = 0;
    return matrix;
  }
  const CertificateGraph before = buildCertificateGraph(state.board);
  for (int disc_offset = 0; disc_offset < kBoardSize; ++disc_offset) {
    int safe_columns = 0;
    int productive_columns = 0;
    int best_release = 0;
    int best_quality = std::numeric_limits<int>::min();
    for (int column = 0; column < kBoardSize; ++column) {
      KeyCertificate& key = matrix.keys[disc_offset][column];
      if (!isLegal(state.board, column)) continue;
      key.legal = true;
      const ConservativeResult release = conservativePlay(
          state, column, static_cast<std::uint8_t>(disc_offset + 1));
      ++simulations;
      if (!release.played) {
        throw std::runtime_error("conservative key rejected legal column");
      }
      const CertificateGraph after = buildCertificateGraph(release.board);
      key.survives = !release.terminal;
      key.clears = release.clears;
      key.reveals = release.reveals;
      key.cracks = release.cracks;
      key.waves = release.waves;
      key.maximum_depth = release.maximum_depth;
      key.top_slack = after.stats.top_slack;
      key.build_gain = after.stats.stored_mass - before.stats.stored_mass;
      key.frontier_gain =
          after.stats.frontier_access - before.stats.frontier_access;
      key.clog_improvement =
          before.stats.clog_debt - after.stats.clog_debt;
      key.release_strength = 12 * key.clears + 18 * key.reveals +
                             7 * key.cracks + 3 * key.waves +
                             key.maximum_depth;
      const int constructive = std::clamp(
          key.build_gain + key.frontier_gain + 2 * key.clog_improvement,
          -63, 63);
      key.quality = key.release_strength * 128 + constructive;
      key.productive = key.survives &&
                       (key.release_strength > 0 || key.build_gain >= 8 ||
                        key.clog_improvement >= 4);
      safe_columns += key.survives;
      productive_columns += key.productive;
      if (key.survives) {
        best_release = std::max(best_release, key.release_strength);
        best_quality = std::max(best_quality, key.quality);
      }
      matrix.summary.strong_keys +=
          key.survives && key.release_strength >= 24;
    }
    if (best_quality == std::numeric_limits<int>::min()) best_quality = 0;
    matrix.summary.worst_safe_columns =
        std::min(matrix.summary.worst_safe_columns, safe_columns);
    matrix.summary.worst_productive_columns =
        std::min(matrix.summary.worst_productive_columns,
                 productive_columns);
    matrix.summary.worst_best_release =
        std::min(matrix.summary.worst_best_release, best_release);
    matrix.summary.worst_best_quality =
        std::min(matrix.summary.worst_best_quality, best_quality);
    matrix.summary.total_safe_columns += safe_columns;
    matrix.summary.total_productive_columns += productive_columns;
    matrix.summary.total_best_release += best_release;
    matrix.summary.total_best_quality += best_quality;
  }
  if (matrix.summary.worst_best_release == std::numeric_limits<int>::max()) {
    matrix.summary.worst_best_release = 0;
  }
  if (matrix.summary.worst_best_quality == std::numeric_limits<int>::max()) {
    matrix.summary.worst_best_quality = 0;
  }
  return matrix;
}

enum class OptionMode : std::uint8_t {
  kCharge,
  kDig,
  kRelease,
  kRepair,
  kEmergency,
  kCount,
};

std::string_view optionName(OptionMode option) {
  switch (option) {
    case OptionMode::kCharge:
      return "charge";
    case OptionMode::kDig:
      return "dig";
    case OptionMode::kRelease:
      return "release";
    case OptionMode::kRepair:
      return "repair";
    case OptionMode::kEmergency:
      return "emergency";
    case OptionMode::kCount:
      break;
  }
  throw std::invalid_argument("invalid viability-reservoir option");
}

struct KnownDiscKeys {
  int best_clears = 0;
  int best_damage = 0;
  int best_waves = 0;
  int productive_columns = 0;
};

KnownDiscKeys knownDiscKeys(const TriggerMatrix& matrix, int disc) {
  if (disc < 1 || disc > kBoardSize) {
    throw std::invalid_argument("invalid known disc for trigger matrix");
  }
  KnownDiscKeys result;
  for (const KeyCertificate& key : matrix.keys[disc - 1]) {
    if (!key.legal) continue;
    result.best_clears = std::max(result.best_clears, key.clears);
    result.best_damage =
        std::max(result.best_damage, key.reveals + key.cracks);
    result.best_waves = std::max(result.best_waves, key.waves);
    result.productive_columns += key.productive;
  }
  return result;
}

OptionMode selectOption(const PublicState& state,
                        const CertificateGraph& graph,
                        const TriggerMatrix& triggers) {
  const GraphStats& stats = graph.stats;
  const KnownDiscKeys known = knownDiscKeys(triggers, state.next_disc);
  const int projected_occupancy =
      stats.occupied + state.moves_remaining + kBoardSize;

  // Frozen mechanics-derived thresholds: height six leaves one physical row;
  // 32 projected cells leaves less than 2.5 clears/move of slack through the
  // next rise; 16 clog-debt is two adjacent-one pairs or an equivalent low cap.
  if (stats.maximum_height >= 6 ||
      triggers.summary.worst_safe_columns <= 1 ||
      (state.moves_remaining == 1 && stats.occupied >= 27)) {
    return OptionMode::kEmergency;
  }
  if (stats.clog_debt >= 16 && stats.capped_low_columns > 0) {
    return OptionMode::kRepair;
  }
  if (projected_occupancy >= 32 || stats.occupied >= 30 ||
      (known.best_clears >= 3 && known.best_waves >= 2)) {
    return OptionMode::kRelease;
  }
  if (known.best_damage > 0 ||
      (stats.cracked_cells > 0 && stats.frontier_access > 0)) {
    return OptionMode::kDig;
  }
  return OptionMode::kCharge;
}

struct CandidateEvidence {
  int column = -1;
  bool conservative_survives = false;
  int terminal_samples = 0;
  int minimum_top_slack = kBoardSize;
  int minimum_worst_safe_columns = kBoardSize;
  int minimum_worst_productive_columns = kBoardSize;
  int minimum_worst_best_release = std::numeric_limits<int>::max();
  int minimum_worst_best_quality = std::numeric_limits<int>::max();
  std::int64_t score_sum = 0;
  int clears_sum = 0;
  int reveals_sum = 0;
  int cracks_sum = 0;
  int waves_sum = 0;
  int top_slack_sum = 0;
  int occupied_sum = 0;
  int cover_debt_sum = 0;
  int frontier_sum = 0;
  int stored_mass_sum = 0;
  int clog_debt_sum = 0;
  int total_productive_sum = 0;
  int total_quality_sum = 0;
  int conservative_clears = 0;
  int conservative_reveals = 0;
  int conservative_cracks = 0;
  std::uint64_t sampled_transitions = 0;
  std::uint64_t certificate_simulations = 0;
  std::array<std::int64_t, kRankFields> rank{};

  bool operator==(const CandidateEvidence&) const = default;
};

using Rank = std::array<std::int64_t, kRankFields>;

Rank buildRank(OptionMode option, const GraphStats& before,
               const CandidateEvidence& candidate) {
  Rank rank{};
  const int cover_reduction =
      kSuccessorSamples * before.cover_altitude_debt -
      candidate.cover_debt_sum;
  const int clog_reduction =
      kSuccessorSamples * before.clog_debt - candidate.clog_debt_sum;
  const int reservoir_gain =
      candidate.stored_mass_sum - kSuccessorSamples * before.stored_mass;
  rank[0] = candidate.conservative_survives;
  rank[1] = -candidate.terminal_samples;
  rank[2] = candidate.minimum_worst_safe_columns;

  switch (option) {
    case OptionMode::kEmergency:
      rank[3] = candidate.minimum_top_slack;
      rank[4] = candidate.clears_sum;
      rank[5] = candidate.reveals_sum;
      rank[6] = cover_reduction;
      rank[7] = -candidate.occupied_sum;
      rank[8] = -candidate.clog_debt_sum;
      rank[9] = candidate.waves_sum;
      rank[10] = candidate.score_sum;
      rank[11] = candidate.minimum_worst_best_quality;
      rank[12] = candidate.total_productive_sum;
      break;
    case OptionMode::kRepair:
      rank[3] = clog_reduction;
      rank[4] = -candidate.clog_debt_sum;
      rank[5] = candidate.clears_sum;
      rank[6] = candidate.minimum_worst_productive_columns;
      rank[7] = candidate.reveals_sum;
      rank[8] = candidate.minimum_top_slack;
      rank[9] = reservoir_gain;
      rank[10] = cover_reduction;
      rank[11] = candidate.score_sum;
      break;
    case OptionMode::kRelease:
      rank[3] = candidate.clears_sum;
      rank[4] = candidate.reveals_sum;
      rank[5] = candidate.waves_sum;
      rank[6] = candidate.minimum_top_slack;
      rank[7] = candidate.minimum_worst_best_release;
      rank[8] = cover_reduction;
      rank[9] = -candidate.clog_debt_sum;
      rank[10] = candidate.total_productive_sum;
      rank[11] = candidate.score_sum;
      break;
    case OptionMode::kDig:
      rank[3] = candidate.reveals_sum;
      rank[4] = candidate.conservative_reveals;
      rank[5] = candidate.cracks_sum;
      rank[6] = candidate.conservative_cracks;
      rank[7] = cover_reduction;
      rank[8] = candidate.clears_sum;
      rank[9] = candidate.minimum_worst_productive_columns;
      rank[10] = reservoir_gain;
      rank[11] = -candidate.clog_debt_sum;
      rank[12] = candidate.score_sum;
      break;
    case OptionMode::kCharge:
      rank[3] = candidate.minimum_worst_productive_columns;
      rank[4] = candidate.minimum_worst_best_quality;
      rank[5] = candidate.total_quality_sum;
      rank[6] = reservoir_gain;
      rank[7] = candidate.reveals_sum + candidate.cracks_sum -
                candidate.clears_sum;
      rank[8] = -candidate.clog_debt_sum;
      rank[9] = cover_reduction;
      rank[10] = candidate.minimum_top_slack;
      rank[11] = candidate.total_productive_sum;
      rank[12] = candidate.score_sum;
      break;
    case OptionMode::kCount:
      throw std::invalid_argument("cannot rank sentinel option");
  }
  rank[13] = candidate.top_slack_sum;
  rank[14] = candidate.frontier_sum;
  rank[15] = -candidate.occupied_sum;
  rank[16] = -candidate.column;
  rank[17] = 1;
  return rank;
}

struct Decision {
  int action = -1;
  OptionMode option = OptionMode::kEmergency;
  std::array<CandidateEvidence, kBoardSize> candidates{};
  std::uint64_t sampled_transitions = 0;
  std::uint64_t certificate_simulations = 0;

  bool operator==(const Decision&) const = default;
};

Decision chooseActionCanonical(const PublicState& state) {
  Decision result;
  if (state.terminal) return result;
  const CertificateGraph before = buildCertificateGraph(state.board);
  std::uint64_t matrix_work = 0;
  const TriggerMatrix current = buildTriggerMatrix(state, matrix_work);
  result.certificate_simulations += matrix_work;
  result.option = selectOption(state, before, current);
  const State engine_state = materialize(state);
  const std::uint32_t chance_seed =
      detail::scenarioSeedForState(engine_state, kPolicySeed, 1);

  int selected = -1;
  Rank best_rank{};
  bool have_best = false;
  for (const int column : kColumnOrder) {
    CandidateEvidence candidate;
    candidate.column = column;
    if (!isLegal(state.board, column)) {
      result.candidates[column] = candidate;
      continue;
    }

    const ConservativeResult conservative =
        conservativePlay(state, column, state.next_disc);
    ++candidate.certificate_simulations;
    candidate.conservative_survives =
        conservative.played && !conservative.terminal;
    candidate.conservative_clears = conservative.clears;
    candidate.conservative_reveals = conservative.reveals;
    candidate.conservative_cracks = conservative.cracks;

    for (int sample = 0; sample < kSuccessorSamples; ++sample) {
      detail::StratifiedRandom random{
          chance_seed, sample, kSuccessorSamples, 0};
      MoveResult move;
      if (!detail::playMoveSampled(engine_state, column, random, move)) {
        throw std::runtime_error("sampled root rejected legal action");
      }
      ++candidate.sampled_transitions;
      candidate.score_sum += move.score_delta;
      for (const Wave& wave : move.waves) {
        candidate.clears_sum += wave.cleared;
        candidate.reveals_sum += wave.revealed;
      }
      candidate.waves_sum += static_cast<int>(move.waves.size());
      candidate.terminal_samples += move.state.game_over;

      const CertificateGraph after = buildCertificateGraph(move.state.board);
      candidate.minimum_top_slack =
          std::min(candidate.minimum_top_slack, after.stats.top_slack);
      candidate.top_slack_sum += after.stats.top_slack;
      candidate.occupied_sum += after.stats.occupied;
      candidate.cover_debt_sum += after.stats.cover_altitude_debt;
      candidate.frontier_sum += after.stats.frontier_access;
      candidate.stored_mass_sum += after.stats.stored_mass;
      candidate.clog_debt_sum += after.stats.clog_debt;

      if (move.state.game_over) {
        candidate.minimum_worst_safe_columns = 0;
        candidate.minimum_worst_productive_columns = 0;
        candidate.minimum_worst_best_release = 0;
        candidate.minimum_worst_best_quality = 0;
        continue;
      }
      move.state.score = 0;
      move.state.level = 1;
      move.state.moves_played = 0;
      move.state.next_disc = detail::sampledNextDisc(
          chance_seed, sample, kSuccessorSamples);
      std::uint64_t work = 0;
      const TriggerMatrix future =
          buildTriggerMatrix(publicState(move.state), work);
      candidate.certificate_simulations += work;
      candidate.minimum_worst_safe_columns =
          std::min(candidate.minimum_worst_safe_columns,
                   future.summary.worst_safe_columns);
      candidate.minimum_worst_productive_columns =
          std::min(candidate.minimum_worst_productive_columns,
                   future.summary.worst_productive_columns);
      candidate.minimum_worst_best_release =
          std::min(candidate.minimum_worst_best_release,
                   future.summary.worst_best_release);
      candidate.minimum_worst_best_quality =
          std::min(candidate.minimum_worst_best_quality,
                   future.summary.worst_best_quality);
      candidate.total_productive_sum +=
          future.summary.total_productive_columns;
      candidate.total_quality_sum += future.summary.total_best_quality;
    }
    if (candidate.minimum_worst_best_release ==
        std::numeric_limits<int>::max()) {
      candidate.minimum_worst_best_release = 0;
    }
    if (candidate.minimum_worst_best_quality ==
        std::numeric_limits<int>::max()) {
      candidate.minimum_worst_best_quality = 0;
    }
    candidate.rank = buildRank(result.option, before.stats, candidate);
    result.sampled_transitions += candidate.sampled_transitions;
    result.certificate_simulations += candidate.certificate_simulations;
    result.candidates[column] = candidate;
    if (!have_best || candidate.rank > best_rank) {
      have_best = true;
      best_rank = candidate.rank;
      selected = column;
    }
  }
  if (selected < 0) selected = centerFirstMove(state.board);
  result.action = selected;
  return result;
}

Decision chooseAction(const PublicState& source) {
  if (source.terminal) return {};
  bool mirrored = false;
  const PublicState canonical = canonicalState(source, mirrored);
  Decision result = chooseActionCanonical(canonical);
  if (!mirrored) return result;
  std::array<CandidateEvidence, kBoardSize> source_candidates{};
  for (int column = 0; column < kBoardSize; ++column) {
    source_candidates[kBoardSize - 1 - column] = result.candidates[column];
    if (source_candidates[kBoardSize - 1 - column].column >= 0) {
      source_candidates[kBoardSize - 1 - column].column =
          kBoardSize - 1 - result.candidates[column].column;
    }
  }
  result.candidates = source_candidates;
  result.action = kBoardSize - 1 - result.action;
  return result;
}

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

struct BaselineDecision {
  int action = -1;
  std::uint64_t work = 0;
  bool complete = false;
};

BaselineDecision chooseFairDepthOne(const PublicState& source) {
  BaselineDecision result;
  if (source.terminal) return result;
  bool mirrored = false;
  const PublicState canonical_public = canonicalState(source, mirrored);
  const State canonical = materialize(canonical_public);
  fair::SearchContext context;
  const fair::RootEvaluation root = fair::rootDecision(canonical, 1, context);
  int legal = 0;
  int evaluated = 0;
  for (int column = 0; column < kBoardSize; ++column) {
    legal += isLegal(canonical.board, column);
    evaluated += std::isfinite(root.values[column]);
  }
  if (root.action < 0 || legal != evaluated || context.work > 70 ||
      !context.cache.empty()) {
    throw std::runtime_error("fair D1 did not complete exactly");
  }
  result.action = mirrored ? kBoardSize - 1 - root.action : root.action;
  result.work = context.work;
  result.complete = true;
  return result;
}

enum class Policy : std::uint8_t { kViability, kFairD1, kFairD4 };

bool allowedStageASeed(std::uint32_t seed) {
  return seed >= kStageASeedStart && seed < kStageASeedEndExclusive &&
         (seed >> 24u) != 0x4du && (seed >> 24u) != 0x7du &&
         (seed >> 24u) != 0xd7u;
}

void requireStageASeed(std::uint32_t seed) {
  if (!allowedStageASeed(seed)) {
    throw std::invalid_argument("seed is outside the frozen 0x3d65c Stage-A bank");
  }
}

struct GameResult {
  std::uint32_t seed = 0;
  std::int64_t score = 0;
  int moves = 0;
  bool natural_terminal = false;
  bool capped = false;
  int clears = 0;
  int reveals = 0;
  int waves = 0;
  int maximum_chain = 0;
  std::array<std::uint64_t, static_cast<std::size_t>(OptionMode::kCount)>
      option_counts{};
  std::uint64_t sampled_transitions = 0;
  std::uint64_t certificate_simulations = 0;
  std::uint64_t search_work = 0;
  std::uint64_t disc_hash = 0xcbf2'9ce4'8422'2325ull;
  std::int64_t graph_stored_sum = 0;
  std::int64_t graph_clog_sum = 0;
  std::int64_t graph_cover_sum = 0;
  std::int64_t graph_frontier_sum = 0;

  bool operator==(const GameResult&) const = default;
};

void observeDisc(GameResult& result, std::uint8_t disc) {
  result.disc_hash ^= disc;
  result.disc_hash *= 0x0000'0100'0000'01b3ull;
}

void observeMove(GameResult& result, const MoveResult& move) {
  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.waves += static_cast<int>(move.waves.size());
}

GameResult playGame(std::uint32_t seed, Policy policy,
                    const Deadline& deadline) {
  requireStageASeed(seed);
  State state = initialHeadlessState(seed);
  GameResult result;
  result.seed = seed;
  while (!state.game_over && state.moves_played < kMaximumMoves) {
    deadline.check();
    enforceRssLimit();
    observeDisc(result, state.next_disc);
    const PublicState public_state = publicState(state);
    const CertificateGraph graph = buildCertificateGraph(state.board);
    result.graph_stored_sum += graph.stats.stored_mass;
    result.graph_clog_sum += graph.stats.clog_debt;
    result.graph_cover_sum += graph.stats.cover_altitude_debt;
    result.graph_frontier_sum += graph.stats.frontier_access;

    int action = -1;
    if (policy == Policy::kViability) {
      const Decision decision = chooseAction(public_state);
      action = decision.action;
      ++result.option_counts[static_cast<std::size_t>(decision.option)];
      result.sampled_transitions += decision.sampled_transitions;
      result.certificate_simulations += decision.certificate_simulations;
    } else if (policy == Policy::kFairD1) {
      const BaselineDecision decision = chooseFairDepthOne(public_state);
      if (!decision.complete) {
        throw std::runtime_error("fair D1 returned incomplete gameplay move");
      }
      action = decision.action;
      result.search_work += decision.work;
    } else {
      State metadata_free = materialize(public_state);
      const d4::SearchDecision decision = d4::chooseDepth4Action(metadata_free);
      if (!decision.complete) {
        throw std::runtime_error("fair D4 returned incomplete gameplay move");
      }
      action = decision.action;
      result.search_work += decision.work;
    }
    if (!isLegal(state.board, action)) {
      throw std::runtime_error("policy selected an illegal action");
    }
    MoveResult move;
    if (!playHeadlessMove(state, seed, action, move)) {
      throw std::runtime_error("headless engine rejected policy action");
    }
    observeMove(result, move);
  }
  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_moves = 0.0;
  double clears_per_move = 0.0;
  double reveals_per_move = 0.0;
  double waves_per_move = 0.0;
  double stored_mass_per_state = 0.0;
  double clog_debt_per_state = 0.0;
  double cover_debt_per_state = 0.0;
  double frontier_per_state = 0.0;
  int natural_terminals = 0;
  int capped = 0;
  int maximum_chain = 0;
  std::array<std::uint64_t, static_cast<std::size_t>(OptionMode::kCount)>
      option_counts{};
  std::uint64_t sampled_transitions = 0;
  std::uint64_t certificate_simulations = 0;
  std::uint64_t search_work = 0;
};

Summary summarize(const std::vector<GameResult>& games) {
  if (games.empty()) throw std::invalid_argument("cannot summarize no games");
  Summary result;
  std::vector<int> moves;
  std::int64_t total_score = 0;
  std::int64_t total_moves = 0;
  std::int64_t total_clears = 0;
  std::int64_t total_reveals = 0;
  std::int64_t total_waves = 0;
  std::int64_t stored = 0;
  std::int64_t clog = 0;
  std::int64_t cover = 0;
  std::int64_t frontier = 0;
  moves.reserve(games.size());
  for (const GameResult& game : games) {
    total_score += game.score;
    total_moves += game.moves;
    total_clears += game.clears;
    total_reveals += game.reveals;
    total_waves += game.waves;
    stored += game.graph_stored_sum;
    clog += game.graph_clog_sum;
    cover += game.graph_cover_sum;
    frontier += game.graph_frontier_sum;
    result.natural_terminals += game.natural_terminal;
    result.capped += game.capped;
    result.maximum_chain = std::max(result.maximum_chain, game.maximum_chain);
    for (std::size_t option = 0; option < result.option_counts.size();
         ++option) {
      result.option_counts[option] += game.option_counts[option];
    }
    result.sampled_transitions += game.sampled_transitions;
    result.certificate_simulations += game.certificate_simulations;
    result.search_work += game.search_work;
    moves.push_back(game.moves);
  }
  const double count = static_cast<double>(games.size());
  result.mean_score = static_cast<double>(total_score) / count;
  result.mean_moves = static_cast<double>(total_moves) / count;
  if (total_moves > 0) {
    result.clears_per_move =
        static_cast<double>(total_clears) / static_cast<double>(total_moves);
    result.reveals_per_move =
        static_cast<double>(total_reveals) / static_cast<double>(total_moves);
    result.waves_per_move =
        static_cast<double>(total_waves) / static_cast<double>(total_moves);
    result.stored_mass_per_state =
        static_cast<double>(stored) / static_cast<double>(total_moves);
    result.clog_debt_per_state =
        static_cast<double>(clog) / static_cast<double>(total_moves);
    result.cover_debt_per_state =
        static_cast<double>(cover) / static_cast<double>(total_moves);
    result.frontier_per_state =
        static_cast<double>(frontier) / static_cast<double>(total_moves);
  }
  std::sort(moves.begin(), moves.end());
  const std::size_t bottom_count = std::max<std::size_t>(1, games.size() / 4);
  result.bottom_quartile_moves =
      static_cast<double>(std::accumulate(moves.begin(),
                                          moves.begin() + bottom_count, 0LL)) /
      static_cast<double>(bottom_count);
  return result;
}

std::vector<GameResult> evaluate(Policy policy, int threads,
                                 const Deadline& deadline) {
  if (threads < 1 || threads > 8) {
    throw std::invalid_argument("threads must be in [1, 8]");
  }
  std::vector<GameResult> games(kStageAGames);
  std::atomic<int> next{0};
  const int workers = std::min(threads, kStageAGames);
  std::vector<std::future<void>> futures;
  futures.reserve(workers);
  for (int worker = 0; worker < workers; ++worker) {
    futures.push_back(std::async(std::launch::async, [&] {
      for (;;) {
        const int index = next.fetch_add(1);
        if (index >= kStageAGames) return;
        games[index] = playGame(
            kStageASeedStart + static_cast<std::uint32_t>(index), policy,
            deadline);
      }
    }));
  }
  for (auto& future : futures) future.get();
  deadline.check();
  enforceRssLimit();
  return games;
}

struct PairedSummary {
  int candidate_score_wins = 0;
  int candidate_move_wins = 0;
  int candidate_joint_wins = 0;
  double mean_score_delta = 0.0;
  double mean_move_delta = 0.0;
};

PairedSummary pair(const std::vector<GameResult>& candidate,
                   const std::vector<GameResult>& baseline) {
  if (candidate.size() != baseline.size() || candidate.empty()) {
    throw std::invalid_argument("paired cohorts do not align");
  }
  PairedSummary result;
  for (std::size_t index = 0; index < candidate.size(); ++index) {
    // The headless next-disc tape is action-independent, but games with
    // different lifetimes necessarily hash different-length prefixes.  Seed
    // identity is therefore the exact pairing invariant; full-stream hashes
    // remain useful only for equal-length determinism checks.
    if (candidate[index].seed != baseline[index].seed) {
      throw std::runtime_error("paired seed mismatch");
    }
    if (candidate[index].moves == baseline[index].moves &&
        candidate[index].disc_hash != baseline[index].disc_hash) {
      throw std::runtime_error("equal-length paired disc-stream mismatch");
    }
    const bool score_win = candidate[index].score > baseline[index].score;
    const bool move_win = candidate[index].moves > baseline[index].moves;
    result.candidate_score_wins += score_win;
    result.candidate_move_wins += move_win;
    result.candidate_joint_wins += score_win && move_win;
    result.mean_score_delta +=
        static_cast<double>(candidate[index].score - baseline[index].score);
    result.mean_move_delta += candidate[index].moves - baseline[index].moves;
  }
  result.mean_score_delta /= static_cast<double>(candidate.size());
  result.mean_move_delta /= static_cast<double>(candidate.size());
  return result;
}

struct Options {
  std::string output = "/tmp/drop7-viability-reservoir-stage-a.json";
  int threads = 4;
};

Options parseOptions(int argc, char** argv, int begin) {
  Options options;
  for (int index = begin; index < argc; ++index) {
    const std::string_view argument(argv[index]);
    if (argument == "--output" && index + 1 < argc) {
      options.output = argv[++index];
    } else if (argument == "--threads" && index + 1 < argc) {
      options.threads = std::stoi(argv[++index]);
    } else {
      throw std::invalid_argument("unknown or incomplete option");
    }
  }
  if (options.output.empty()) throw std::invalid_argument("empty output path");
  if (options.threads < 1 || options.threads > 8) {
    throw std::invalid_argument("threads must be in [1, 8]");
  }
  return options;
}

void writeSummary(std::ostream& output, const Summary& summary) {
  output << "{\"meanScore\":" << summary.mean_score
         << ",\"meanMoves\":" << summary.mean_moves
         << ",\"bottomQuartileMoves\":" << summary.bottom_quartile_moves
         << ",\"clearsPerMove\":" << summary.clears_per_move
         << ",\"revealsPerMove\":" << summary.reveals_per_move
         << ",\"wavesPerMove\":" << summary.waves_per_move
         << ",\"storedMassPerState\":" << summary.stored_mass_per_state
         << ",\"clogDebtPerState\":" << summary.clog_debt_per_state
         << ",\"coverDebtPerState\":" << summary.cover_debt_per_state
         << ",\"frontierPerState\":" << summary.frontier_per_state
         << ",\"naturalTerminals\":" << summary.natural_terminals
         << ",\"capped\":" << summary.capped
         << ",\"maximumChain\":" << summary.maximum_chain
         << ",\"sampledTransitions\":" << summary.sampled_transitions
         << ",\"certificateSimulations\":"
         << summary.certificate_simulations
         << ",\"searchWork\":" << summary.search_work
         << ",\"options\":{";
  for (std::size_t option = 0; option < summary.option_counts.size(); ++option) {
    if (option > 0) output << ',';
    output << '\"'
           << optionName(static_cast<OptionMode>(option)) << "\":"
           << summary.option_counts[option];
  }
  output << "}}";
}

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

void writeArtifact(const Options& options,
                   const std::vector<GameResult>& candidate,
                   const Summary& candidate_summary,
                   const std::vector<GameResult>& fair_d1,
                   const Summary& fair_d1_summary,
                   const PairedSummary& d1_paired, bool absolute_gate,
                   const std::vector<GameResult>* fair_d4,
                   const Summary* fair_d4_summary,
                   const PairedSummary* d4_paired, bool passed,
                   double wall_seconds) {
  std::ofstream output(options.output);
  if (!output) throw std::runtime_error("could not open Stage-A artifact");
  output << std::setprecision(12)
         << "{\n  \"format\":\"drop7-viability-reservoir-stage-a-v1\","
         << "\n  \"seedStart\":\"0x3d65c000\","
         << "\n  \"games\":" << kStageAGames
         << ",\n  \"maximumMoves\":" << kMaximumMoves
         << ",\n  \"publicOnly\":true,"
         << "\n  \"conservativeInertReveals\":true,"
         << "\n  \"triggerMatrix\":[7,7],"
         << "\n  \"parameters\":0,"
         << "\n  \"fittingSeedsOpened\":false,"
         << "\n  \"candidate\":";
  writeSummary(output, candidate_summary);
  output << ",\n  \"fairD1\":";
  writeSummary(output, fair_d1_summary);
  output << ",\n  \"d1Paired\":{\"scoreWins\":"
         << d1_paired.candidate_score_wins << ",\"moveWins\":"
         << d1_paired.candidate_move_wins << ",\"jointWins\":"
         << d1_paired.candidate_joint_wins << ",\"meanScoreDelta\":"
         << d1_paired.mean_score_delta << ",\"meanMoveDelta\":"
         << d1_paired.mean_move_delta << "},"
         << "\n  \"absoluteGate\":" << (absolute_gate ? "true" : "false")
         << ",\n  \"d4Opened\":" << (fair_d4 ? "true" : "false");
  if (fair_d4 && fair_d4_summary && d4_paired) {
    output << ",\n  \"fairD4\":";
    writeSummary(output, *fair_d4_summary);
    output << ",\n  \"d4Paired\":{\"scoreWins\":"
           << d4_paired->candidate_score_wins << ",\"moveWins\":"
           << d4_paired->candidate_move_wins << ",\"jointWins\":"
           << d4_paired->candidate_joint_wins << ",\"meanScoreDelta\":"
           << d4_paired->mean_score_delta << ",\"meanMoveDelta\":"
           << d4_paired->mean_move_delta << '}';
  }
  output << ",\n  \"gate\":{\"meanScore\":" << kGateMeanScore
         << ",\"meanMoves\":" << kGateMeanMoves
         << ",\"clearsPerMove\":" << kGateClearsPerMove
         << ",\"revealsPerMove\":" << kGateRevealsPerMove
         << ",\"bottomQuartileMoves\":" << kGateBottomQuartileMoves
         << ",\"jointD4Wins\":" << kGateJointWins << "},"
         << "\n  \"passed\":" << (passed ? "true" : "false")
         << ",\n  \"wallSeconds\":" << wall_seconds
         << ",\n  \"peakRssBytes\":" << peakRssBytes()
         << ",\n  \"candidateGames\":[";
  for (std::size_t index = 0; index < candidate.size(); ++index) {
    if (index > 0) output << ',';
    writeGame(output, candidate[index]);
  }
  output << "],\n  \"fairD1Games\":[";
  for (std::size_t index = 0; index < fair_d1.size(); ++index) {
    if (index > 0) output << ',';
    writeGame(output, fair_d1[index]);
  }
  output << ']';
  if (fair_d4) {
    output << ",\n  \"fairD4Games\":[";
    for (std::size_t index = 0; index < fair_d4->size(); ++index) {
      if (index > 0) output << ',';
      writeGame(output, (*fair_d4)[index]);
    }
    output << ']';
  }
  output << "\n}\n";
  if (!output) throw std::runtime_error("failed writing Stage-A artifact");
}

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;
}

PublicState asymmetricFixture() {
  PublicState fixture;
  fixture.board.fill(kEmpty);
  fixture.board[indexOf(6, 0)] = kSolid;
  fixture.board[indexOf(5, 0)] = 4;
  fixture.board[indexOf(6, 1)] = kSolid;
  fixture.board[indexOf(5, 1)] = 1;
  fixture.board[indexOf(6, 2)] = kSolid;
  fixture.board[indexOf(6, 3)] = kCracked;
  fixture.board[indexOf(6, 4)] = kSolid;
  fixture.board[indexOf(5, 4)] = 6;
  fixture.board[indexOf(4, 4)] = 5;
  fixture.next_disc = 3;
  fixture.moves_remaining = 4;
  return fixture;
}

bool selfTest(std::ostream& output) {
  expect(kLevelBonus == 17'000, "corrected Hardcore scoring regression");
  const PublicState fixture = asymmetricFixture();
  int popper_count = 0;
  findPoppers(fixture.board, popper_count);
  expect(popper_count == 0, "asymmetric certificate fixture is unstable");

  const CertificateGraph graph = buildCertificateGraph(fixture.board);
  const CertificateGraph reflected_graph =
      buildCertificateGraph(mirror(fixture).board);
  expect(graph.stats == reflected_graph.stats && graph.node_count > 0 &&
             graph.edge_count > 0 && graph.stats.stored_mass > 0 &&
             graph.stats.frontier_edges > 0,
         "causal certificate graph/reflection fixture failed");

  std::uint64_t work = 0;
  const TriggerMatrix matrix = buildTriggerMatrix(fixture, work);
  std::uint64_t reflected_work = 0;
  const TriggerMatrix reflected_matrix =
      buildTriggerMatrix(mirror(fixture), reflected_work);
  expect(work == 49 && reflected_work == 49 &&
             matrix.summary == reflected_matrix.summary,
         "7x7 trigger matrix work/reflection summary failed");
  for (int disc = 0; disc < kBoardSize; ++disc) {
    for (int column = 0; column < kBoardSize; ++column) {
      expect(matrix.keys[disc][column] ==
                 reflected_matrix.keys[disc][kBoardSize - 1 - column],
             "7x7 trigger certificate reflection failed");
    }
  }

  PublicState crack;
  crack.board.fill(kEmpty);
  crack.board[indexOf(6, 0)] = kSolid;
  crack.next_disc = 1;
  crack.moves_remaining = 5;
  const ConservativeResult cracked = conservativePlay(crack, 0, 1);
  expect(cracked.played && !cracked.terminal && cracked.clears == 1 &&
             cracked.cracks == 1 && cracked.reveals == 0 &&
             cracked.board[indexOf(6, 0)] == kCracked,
         "single-hit conservative cover certificate failed");
  crack.board[indexOf(6, 0)] = kCracked;
  const ConservativeResult revealed = conservativePlay(crack, 0, 1);
  expect(revealed.reveals == 1 && revealed.cracks == 0 &&
             revealed.board[indexOf(6, 0)] == kInertReveal,
         "inert conservative reveal certificate failed");

  const Decision first = chooseAction(fixture);
  const Decision repeated = chooseAction(fixture);
  const Decision reflected = chooseAction(mirror(fixture));
  expect(first == repeated && isLegal(fixture.board, first.action),
         "controller determinism/legality failed");
  expect(reflected.action == kBoardSize - 1 - first.action &&
             reflected.option == first.option,
         "controller action/option reflection failed");
  for (int column = 0; column < kBoardSize; ++column) {
    expect(first.candidates[column].rank ==
               reflected.candidates[kBoardSize - 1 - column].rank,
           "controller rank reflection failed");
  }

  State metadata = materialize(fixture);
  metadata.score = 9'999'999;
  metadata.level = 777;
  metadata.moves_played = 888;
  expect(publicState(metadata) == fixture &&
             chooseAction(publicState(metadata)) == first,
         "controller used hidden score/level/history metadata");
  PublicState terminal = fixture;
  terminal.terminal = true;
  expect(chooseAction(terminal).action == -1,
         "terminal public state selected an action");

  PublicState emergency = fixture;
  emergency.board[indexOf(3, 4)] = 7;
  emergency.board[indexOf(2, 4)] = 7;
  emergency.board[indexOf(1, 4)] = 7;
  expect(selectOption(emergency, buildCertificateGraph(emergency.board),
                      buildTriggerMatrix(emergency, work)) ==
             OptionMode::kEmergency,
         "emergency option threshold failed");
  PublicState charge;
  charge.board = initialBoard();
  charge.next_disc = 4;
  charge.moves_remaining = 5;
  std::uint64_t charge_work = 0;
  const TriggerMatrix charge_matrix = buildTriggerMatrix(charge, charge_work);
  expect(selectOption(charge, buildCertificateGraph(charge.board),
                      charge_matrix) == OptionMode::kCharge,
         "charge option fixture failed");

  PublicState dig = charge;
  dig.next_disc = 1;
  std::uint64_t dig_work = 0;
  const TriggerMatrix dig_matrix = buildTriggerMatrix(dig, dig_work);
  expect(selectOption(dig, buildCertificateGraph(dig.board), dig_matrix) ==
             OptionMode::kDig,
         "dig option fixture failed");

  PublicState release_state;
  release_state.board.fill(kEmpty);
  for (int row = 2; row < kBoardSize; ++row) {
    for (int column = 0; column < kBoardSize; ++column) {
      release_state.board[indexOf(row, column)] = kSolid;
    }
  }
  release_state.next_disc = 6;
  release_state.moves_remaining = 5;
  std::uint64_t release_work = 0;
  const TriggerMatrix release_matrix =
      buildTriggerMatrix(release_state, release_work);
  expect(selectOption(release_state,
                      buildCertificateGraph(release_state.board),
                      release_matrix) == OptionMode::kRelease,
         "release option fixture failed");

  PublicState repair_state;
  repair_state.board.fill(kEmpty);
  for (int column = 0; column < 2; ++column) {
    for (int row = 3; row < kBoardSize; ++row) {
      repair_state.board[indexOf(row, column)] = kSolid;
    }
    repair_state.board[indexOf(2, column)] = 1;
  }
  repair_state.next_disc = 4;
  repair_state.moves_remaining = 5;
  std::uint64_t repair_work = 0;
  const TriggerMatrix repair_matrix =
      buildTriggerMatrix(repair_state, repair_work);
  expect(selectOption(repair_state,
                      buildCertificateGraph(repair_state.board),
                      repair_matrix) == OptionMode::kRepair,
         "repair option fixture failed");

  PublicState shield_state;
  shield_state.board = initialBoard();
  for (int row = 1; row < kBoardSize - 1; ++row) {
    shield_state.board[indexOf(row, 0)] = kSolid;
  }
  shield_state.next_disc = 4;
  shield_state.moves_remaining = 1;
  const Decision shield = chooseAction(shield_state);
  const ConservativeResult shield_release = conservativePlay(
      shield_state, shield.action, shield_state.next_disc);
  expect(shield.action != 0 && shield_release.played &&
             !shield_release.terminal,
         "hard conservative terminal shield failed");

  const BaselineDecision d1_first = chooseFairDepthOne(fixture);
  const BaselineDecision d1_repeat = chooseFairDepthOne(fixture);
  const BaselineDecision d1_reflected = chooseFairDepthOne(mirror(fixture));
  expect(d1_first.action == d1_repeat.action && d1_first.complete &&
             d1_reflected.action == kBoardSize - 1 - d1_first.action,
         "fair D1 determinism/reflection failed");

  expect(allowedStageASeed(kStageASeedStart) &&
             allowedStageASeed(kStageASeedEndExclusive - 1u) &&
             !allowedStageASeed(kStageASeedStart - 1u) &&
             !allowedStageASeed(kStageASeedEndExclusive) &&
             throwsInvalid([] { requireStageASeed(0x3d65'0000u); }) &&
             throwsInvalid([] { requireStageASeed(0x3d65'1000u); }) &&
             throwsInvalid([] { requireStageASeed(0x3d65'8000u); }) &&
             throwsInvalid([] { requireStageASeed(0x3d65'e000u); }) &&
             throwsInvalid([] { requireStageASeed(0x4d65'c000u); }) &&
             throwsInvalid([] { requireStageASeed(0x7d65'c000u); }) &&
             throwsInvalid([] { requireStageASeed(0xd765'c000u); }),
         "Stage-A seed guards failed");
  enforceRssLimit();
  output << "VIABILITY_RESERVOIR_SELF_TEST {\"passed\":true,"
         << "\"publicOnly\":true,\"metadataBlind\":true,"
         << "\"reflection\":true,\"deterministic\":true,"
         << "\"legal\":true,\"inertRevealConservative\":true,"
         << "\"triggerMatrix\":[7,7],\"certificateWork\":" << work
         << ",\"fittedParameters\":0,\"seedGuards\":true,"
         << "\"peakRssBytes\":" << peakRssBytes() << "}\n";
  return true;
}

int runStageA(const Options& options, std::ostream& output) {
  const Deadline deadline;
  const std::vector<GameResult> candidate =
      evaluate(Policy::kViability, options.threads, deadline);
  const Summary candidate_summary = summarize(candidate);
  const std::vector<GameResult> fair_d1 =
      evaluate(Policy::kFairD1, options.threads, deadline);
  const Summary fair_d1_summary = summarize(fair_d1);
  const PairedSummary d1_paired = pair(candidate, fair_d1);

  // The candidate's fixed absolute score/move thresholds gate the expensive
  // D4 comparison.  Throughput and tail gates cannot enable D4.
  const bool absolute_gate = candidate_summary.mean_score >= kGateMeanScore &&
                             candidate_summary.mean_moves >= kGateMeanMoves;
  std::vector<GameResult> fair_d4;
  Summary fair_d4_summary;
  PairedSummary d4_paired;
  if (absolute_gate) {
    fair_d4 = evaluate(Policy::kFairD4, options.threads, deadline);
    fair_d4_summary = summarize(fair_d4);
    d4_paired = pair(candidate, fair_d4);
  }
  const bool passed =
      absolute_gate &&
      candidate_summary.clears_per_move >= kGateClearsPerMove &&
      candidate_summary.reveals_per_move >= kGateRevealsPerMove &&
      candidate_summary.bottom_quartile_moves >=
          kGateBottomQuartileMoves &&
      d4_paired.candidate_joint_wins >= kGateJointWins;
  deadline.check();
  enforceRssLimit();
  const double wall_seconds = deadline.elapsedSeconds();
  writeArtifact(options, candidate, candidate_summary, fair_d1,
                fair_d1_summary, d1_paired, absolute_gate,
                absolute_gate ? &fair_d4 : nullptr,
                absolute_gate ? &fair_d4_summary : nullptr,
                absolute_gate ? &d4_paired : nullptr, passed, wall_seconds);
  output << std::fixed << std::setprecision(3)
         << "VIABILITY_RESERVOIR_STAGE_A {\"candidateScore\":"
         << candidate_summary.mean_score << ",\"candidateMoves\":"
         << candidate_summary.mean_moves << ",\"bottomQuartileMoves\":"
         << candidate_summary.bottom_quartile_moves
         << ",\"clearsPerMove\":" << candidate_summary.clears_per_move
         << ",\"revealsPerMove\":" << candidate_summary.reveals_per_move
         << ",\"fairD1Score\":" << fair_d1_summary.mean_score
         << ",\"fairD1Moves\":" << fair_d1_summary.mean_moves
         << ",\"d1JointWins\":" << d1_paired.candidate_joint_wins
         << ",\"absoluteGate\":" << (absolute_gate ? "true" : "false")
         << ",\"d4Opened\":" << (absolute_gate ? "true" : "false")
         << ",\"d4JointWins\":" << d4_paired.candidate_joint_wins
         << ",\"passed\":" << (passed ? "true" : "false")
         << ",\"wallSeconds\":" << wall_seconds
         << ",\"peakRssBytes\":" << peakRssBytes()
         << ",\"artifact\":\"" << options.output << "\"}\n";
  return passed ? EXIT_SUCCESS : 2;
}

}  // namespace drop7::viability_reservoir_controller

#ifndef DROP7_VIABILITY_RESERVOIR_CONTROLLER_LIBRARY
int main(int argc, char** argv) {
  try {
    if (argc >= 2 && std::string_view(argv[1]) == "--self-test") {
      return drop7::viability_reservoir_controller::selfTest(std::cout)
                 ? EXIT_SUCCESS
                 : EXIT_FAILURE;
    }
    if (argc >= 2 && std::string_view(argv[1]) == "--stage-a") {
      const auto options =
          drop7::viability_reservoir_controller::parseOptions(argc, argv, 2);
      return drop7::viability_reservoir_controller::runStageA(options,
                                                               std::cout);
    }
    std::cerr << "usage: drop7_viability_reservoir_controller --self-test | "
                 "--stage-a [--output PATH] [--threads N]\n";
    return 2;
  } catch (const std::exception& error) {
    std::cerr << "drop7_viability_reservoir_controller: " << error.what()
              << '\n';
    return EXIT_FAILURE;
  }
}
#endif