Drop7 Research
approaches/d4-long-outcome/rollout-veto/d4-d2-rollout-veto-cache-free.cppC++643 lines · 25.9 KB
#define DROP7_D4_D2_ROLLOUT_VETO_EXACT_COMPRESSED_LIBRARY
#include "d4-d2-rollout-veto-exact-compressed.cpp"
#undef DROP7_D4_D2_ROLLOUT_VETO_EXACT_COMPRESSED_LIBRARY

#include <bit>

// Evaluates the bounded rollout compressor's full-width fair-D2 tree directly,
// without its generic string/LRU transposition cache.  It preserves every
// value, action, tape, gate, and public-state input.
namespace drop7::d4_d2_rollout_veto_cache_free {

namespace compressed =
    drop7::d4_d2_rollout_veto_exact_compressed;
namespace original = drop7::d4_d2_rollout_veto;
namespace d4 = drop7::fair_only_depth4;
namespace fair = drop7::fair_only_horizon;

constexpr std::uint32_t kPilotSeed = compressed::kPilotSeed;
constexpr std::string_view kCompressedArtifact =
    "/tmp/drop7-d4-d2-rollout-veto-exact-compressed.json";
constexpr std::string_view kCompressedArtifactSha256 =
    "93873f5b5351311f3834a02dc676df86d74ded3db0166c6e3d719ee80f24fc45";
constexpr double kCompressedSeconds = 639.320592375;
constexpr std::uint64_t kCompressedD2Calls = 152'884;
constexpr std::uint64_t kCompressedD2Work = 310'321'943;

static_assert(original::kContinuationDepth == 2);
static_assert(original::kWorstD2Work == 2'485);
static_assert(kPilotSeed == 0x3ded'0000u);

struct Options {
  std::string output =
      "/tmp/drop7-d4-d2-rollout-veto-cache-free.json";
  std::string audit_output =
      "/tmp/drop7-d4-d2-rollout-veto-cache-free-audit.json";
  std::string trace_output =
      "/tmp/drop7-d4-d2-rollout-veto-cache-free-trace.jsonl";
};

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 == "--audit-output") {
      result.audit_output = argv[index + 1];
    } else if (argument == "--trace-output") {
      result.trace_output = argv[index + 1];
    } else {
      throw std::invalid_argument("unknown option " + argument);
    }
  }
  return result;
}

struct DirectContext {
  std::uint64_t nodes = 0;
  std::uint64_t work = 0;
};

struct ActionValue {
  double value = 0.0;
  double expected_score = 0.0;
};

double directBestFuture(const State& state, int depth,
                        DirectContext& context);

ActionValue directEvaluateAction(const State& state, int column, int depth,
                                 DirectContext& context) {
  const std::uint32_t state_seed =
      cfpi::detail::scenarioSeedForState(state, fair::kPolicySeed, depth);
  ActionValue result;
  for (int sample = 0; sample < fair::kChanceSamples; ++sample) {
    cfpi::detail::StratifiedRandom random{
        state_seed, sample, fair::kChanceSamples, 0};
    MoveResult move;
    const bool played =
        cfpi::detail::playMoveSampled(state, column, random, move);
    ++context.work;
    if (!played) {
      result.value += fair::kTerminalUtility;
      continue;
    }
    const double score_delta = static_cast<double>(move.score_delta);
    result.expected_score += score_delta;
    if (move.state.game_over) {
      result.value += score_delta + fair::kTerminalUtility;
      continue;
    }
    move.state.score = 0;
    move.state.next_disc = cfpi::detail::sampledNextDisc(
        state_seed, sample, fair::kChanceSamples);
    bool ignored = false;
    const State next = cfpi::detail::canonicalState(move.state, ignored);
    result.value +=
        score_delta + directBestFuture(next, depth - 1, context);
  }
  result.value /= fair::kChanceSamples;
  result.expected_score /= fair::kChanceSamples;
  return result;
}

double directBestFuture(const State& state, int depth,
                        DirectContext& context) {
  ++context.nodes;
  if (state.game_over) return fair::kTerminalUtility;
  if (depth == 0) {
    ++context.work;
    const double value = fair::fairLeaf(state);
    if (!std::isfinite(value)) {
      throw std::runtime_error("cache-free D2 leaf was non-finite");
    }
    return value;
  }
  double best = -std::numeric_limits<double>::infinity();
  for (const int column : cfpi::detail::kColumnOrder) {
    if (!isLegal(state.board, column)) continue;
    best = std::max(
        best, directEvaluateAction(state, column, depth, context).value);
  }
  return std::isfinite(best) ? best : fair::kTerminalUtility;
}

struct FullEvaluation {
  int action = -1;
  double value = -std::numeric_limits<double>::infinity();
  std::array<double, kBoardSize> values{};
  std::array<double, kBoardSize> expected_scores{};
  std::uint64_t nodes = 0;
  std::uint64_t work = 0;
  int root_actions = 0;
};

FullEvaluation cacheFreeFull(const original::ObservableState& source) {
  if (source.game_over) return {};
  bool mirrored = false;
  const State canonical =
      cfpi::detail::canonicalState(original::materialize(source), mirrored);
  FullEvaluation canonical_result;
  canonical_result.values.fill(-std::numeric_limits<double>::infinity());
  canonical_result.expected_scores.fill(
      -std::numeric_limits<double>::infinity());
  DirectContext context;
  for (const int column : cfpi::detail::kColumnOrder) {
    if (!isLegal(canonical.board, column)) continue;
    ++canonical_result.root_actions;
    const ActionValue candidate = directEvaluateAction(
        canonical, column, original::kContinuationDepth, context);
    canonical_result.values[column] = candidate.value;
    canonical_result.expected_scores[column] = candidate.expected_score;
    if (candidate.value > canonical_result.value) {
      canonical_result.value = candidate.value;
      canonical_result.action = column;
    }
  }
  canonical_result.nodes = context.nodes;
  canonical_result.work = context.work;
  if (canonical_result.action < 0 ||
      canonical_result.work > original::kWorstD2Work) {
    throw std::runtime_error("cache-free D2 full-root proof failed");
  }
  if (!mirrored) return canonical_result;
  FullEvaluation reflected = canonical_result;
  reflected.action = kBoardSize - 1 - canonical_result.action;
  for (int column = 0; column < kBoardSize; ++column) {
    reflected.values[kBoardSize - 1 - column] =
        canonical_result.values[column];
    reflected.expected_scores[kBoardSize - 1 - column] =
        canonical_result.expected_scores[column];
  }
  return reflected;
}

int cacheFreeDepthTwoAction(const original::ObservableState& source,
                            original::D2Metrics* aggregate) {
  if (source.game_over) return -1;
  const FullEvaluation result = cacheFreeFull(source);
  if (aggregate != nullptr) {
    ++aggregate->calls;
    aggregate->work += result.work;
    aggregate->nodes += result.nodes;
    aggregate->root_actions +=
        static_cast<std::uint64_t>(result.root_actions);
    aggregate->full_root = aggregate->full_root && result.action >= 0;
  }
  return result.action;
}

FullEvaluation cachedFull(const original::ObservableState& source) {
  bool mirrored = false;
  const State canonical =
      cfpi::detail::canonicalState(original::materialize(source), mirrored);
  fair::SearchContext context;
  const fair::RootEvaluation root =
      fair::rootDecision(canonical, original::kContinuationDepth, context);
  FullEvaluation result;
  result.action = mirrored ? kBoardSize - 1 - root.action : root.action;
  result.value = root.value;
  result.nodes = context.nodes;
  result.work = context.work;
  for (int column = 0; column < kBoardSize; ++column) {
    result.root_actions += isLegal(canonical.board, column);
    const int destination = mirrored ? kBoardSize - 1 - column : column;
    result.values[destination] = root.values[column];
    result.expected_scores[destination] = root.expected_scores[column];
  }
  return result;
}

bool sameFull(const FullEvaluation& first, const FullEvaluation& second) {
  return first.action == second.action && first.value == second.value &&
         first.values == second.values &&
         first.expected_scores == second.expected_scores &&
         first.root_actions == second.root_actions;
}

bool reflectedFull(const FullEvaluation& first,
                   const FullEvaluation& reflected) {
  if (reflected.action != kBoardSize - 1 - first.action ||
      reflected.value != first.value ||
      reflected.root_actions != first.root_actions) {
    return false;
  }
  for (int column = 0; column < kBoardSize; ++column) {
    if (reflected.values[kBoardSize - 1 - column] != first.values[column] ||
        reflected.expected_scores[kBoardSize - 1 - column] !=
            first.expected_scores[column]) {
      return false;
    }
  }
  return true;
}

bool reflectedValues(const FullEvaluation& first,
                     const FullEvaluation& reflected) {
  if (reflected.value != first.value ||
      reflected.root_actions != first.root_actions) {
    return false;
  }
  for (int column = 0; column < kBoardSize; ++column) {
    if (reflected.values[kBoardSize - 1 - column] != first.values[column] ||
        reflected.expected_scores[kBoardSize - 1 - column] !=
            first.expected_scores[column]) {
      return false;
    }
  }
  return true;
}

struct AuditCounters {
  std::uint64_t calls = 0;
  std::uint64_t exact_forward = 0;
  std::uint64_t exact_reflections = 0;
  std::uint64_t reflection_equivariant_actions = 0;
  std::uint64_t symmetric_tie_exceptions = 0;
  std::uint64_t reflection_equivariant_values = 0;
  std::uint64_t symmetric_value_exceptions = 0;
  std::uint64_t direct_work = 0;
  std::uint64_t cached_work = 0;
  std::uint64_t direct_nodes = 0;
  std::uint64_t cached_nodes = 0;
  std::uint64_t digest = 0x4346'4432'4155'4449ull;
};

thread_local AuditCounters* active_audit = nullptr;

void updateDigest(AuditCounters& audit,
                  const original::ObservableState& state,
                  const FullEvaluation& value) {
  audit.digest = original::mix64(
      audit.digest ^ original::publicHash(state) ^
      static_cast<std::uint64_t>(value.action + 1));
  for (const double root : value.values) {
    audit.digest = original::mix64(
        audit.digest ^ std::bit_cast<std::uint64_t>(root));
  }
}

int auditedCacheFreeDepthTwoAction(
    const original::ObservableState& source,
    original::D2Metrics* aggregate) {
  if (active_audit == nullptr) {
    throw std::logic_error("cache-free audit evaluator outside audit");
  }
  const FullEvaluation direct = cacheFreeFull(source);
  const FullEvaluation cached = cachedFull(source);
  const original::ObservableState reflected_state = original::mirror(source);
  const FullEvaluation reflected = cacheFreeFull(reflected_state);
  const FullEvaluation reflected_cached = cachedFull(reflected_state);
  ++active_audit->calls;
  if (!sameFull(direct, cached)) {
    throw std::runtime_error("cache-free D2 diverged from cached D2");
  }
  ++active_audit->exact_forward;
  if (!sameFull(reflected, reflected_cached)) {
    throw std::runtime_error(
        "cache-free D2 diverged from cached D2 on reflection");
  }
  ++active_audit->exact_reflections;
  const bool symmetric =
      original::sameObservable(source, reflected_state);
  if (reflectedValues(direct, reflected)) {
    ++active_audit->reflection_equivariant_values;
  } else if (symmetric) {
    ++active_audit->symmetric_value_exceptions;
  } else {
    throw std::runtime_error(
        "asymmetric cache-free D2 reflected values diverged");
  }
  if (reflected.action == kBoardSize - 1 - direct.action) {
    ++active_audit->reflection_equivariant_actions;
  } else if (symmetric) {
    ++active_audit->symmetric_tie_exceptions;
  } else {
    throw std::runtime_error("asymmetric cache-free D2 action did not reflect");
  }
  active_audit->direct_work += direct.work;
  active_audit->cached_work += cached.work;
  active_audit->direct_nodes += direct.nodes;
  active_audit->cached_nodes += cached.nodes;
  updateDigest(*active_audit, source, direct);
  if (aggregate != nullptr) {
    ++aggregate->calls;
    aggregate->work += direct.work;
    aggregate->nodes += direct.nodes;
    aggregate->root_actions +=
        static_cast<std::uint64_t>(direct.root_actions);
    aggregate->full_root = aggregate->full_root && direct.action >= 0;
  }
  return direct.action;
}

struct ReplayResult {
  compressed::GameResult game{};
  std::vector<compressed::TraceRecord> records;
};

ReplayResult replayWith(compressed::D2ActionMemo::Evaluator evaluator,
                        std::string_view label) {
  const auto started = std::chrono::steady_clock::now();
  State state = initialHeadlessState(kPilotSeed);
  ReplayResult result;
  result.game.seed = kPilotSeed;
  compressed::D2ActionMemo action_memo(compressed::kActionMemoCapacity,
                                       evaluator);
  while (!state.game_over && state.moves_played < original::kMaximumMoves) {
    compressed::Decision decision =
        compressed::chooseAction(state, action_memo);
    result.game.d4_work += decision.search.work;
    result.game.d4_nodes += decision.search.nodes;
    result.game.d4_cache_hits += decision.search.cache_hits;
    result.game.peak_d4_cache_entries =
        std::max(result.game.peak_d4_cache_entries,
                 decision.search.cache_entries);
    result.game.d4_seconds += decision.d4_seconds;
    result.game.rollout_seconds += decision.rollout_seconds;
    result.game.routed_decisions += decision.routed;
    result.game.switches += decision.switched;
    result.game.passing_alternatives += decision.passing_alternatives;
    result.game.root_q_rejections += decision.root_q_rejections;
    if (decision.routed) {
      compressed::addCompression(result.game.compression,
                                 decision.rollout.metrics);
      result.records.push_back(
          {original::observable(state), std::move(decision)});
    }
    MoveResult move;
    const int action = decision.action;
    if (!playHeadlessMove(state, kPilotSeed, action, move)) {
      throw std::runtime_error("cache-free replay transition failed");
    }
    result.game.maximum_chain =
        std::max(result.game.maximum_chain,
                 static_cast<int>(move.waves.size()));
    for (const Wave& wave : move.waves) {
      result.game.numbered_cleared +=
          static_cast<std::uint64_t>(wave.cleared);
      result.game.covers_revealed +=
          static_cast<std::uint64_t>(wave.revealed);
    }
    if (state.moves_played % 25 == 0) {
      std::cerr << label << " move " << state.moves_played << ", routes "
                << result.game.routed_decisions << ", switches "
                << result.game.switches << '\n';
    }
  }
  result.game.score = state.score;
  result.game.moves = state.moves_played;
  result.game.censored = !state.game_over;
  result.game.elapsed_seconds = std::chrono::duration<double>(
                                    std::chrono::steady_clock::now() - started)
                                    .count();
  result.game.peak_rss_bytes = d4::peakRssBytes();
  if (result.game.score != 404'047 || result.game.moves != 250 ||
      result.game.censored || result.game.numbered_cleared != 569 ||
      result.game.covers_revealed != 329 ||
      result.game.routed_decisions != 179 || result.game.switches != 12 ||
      result.game.passing_alternatives != 15 ||
      result.game.d4_work != 353'804'442 || result.records.size() != 179) {
    throw std::runtime_error("cache-free replay diverged from frozen pilot");
  }
  return result;
}

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

bool selfTest(std::ostream& output) {
  const bool dependency = compressed::selfTest(output);
  const original::ObservableState fixture = compressed::routedFixture();
  const FullEvaluation cached = cachedFull(fixture);
  const FullEvaluation direct = cacheFreeFull(fixture);
  const FullEvaluation reflected =
      cacheFreeFull(original::mirror(fixture));
  original::D2Metrics metrics;
  const int action = cacheFreeDepthTwoAction(fixture, &metrics);
  State state = original::materialize(fixture);
  compressed::D2ActionMemo memo(compressed::kActionMemoCapacity,
                                &cacheFreeDepthTwoAction);
  const compressed::Decision decision =
      compressed::chooseAction(state, memo, true, 3);
  compressed::D2ActionMemo reference_memo;
  const compressed::Decision reference =
      compressed::chooseAction(state, reference_memo, true, 3);
  const bool exact = sameFull(cached, direct) && action == cached.action &&
                     decision.action == reference.action &&
                     decision.rollout.actions == reference.rollout.actions &&
                     decision.rollout.evaluated ==
                         reference.rollout.evaluated;
  const bool reflection = reflectedFull(direct, reflected);
  const bool resources = metrics.calls == 1 && metrics.work <= 2'485 &&
                         metrics.peak_cache_entries == 0 &&
                         metrics.cache_hits == 0 && metrics.full_root;
  const bool passed = dependency && exact && reflection && resources;
  output << std::setprecision(12)
         << "D4_D2_CACHE_FREE_SELF_TEST {\"passed\":"
         << (passed ? "true" : "false")
         << ",\"dependency\":" << (dependency ? "true" : "false")
         << ",\"bitExactValuesAndAction\":"
         << (exact ? "true" : "false")
         << ",\"reflectionExact\":"
         << (reflection ? "true" : "false")
         << ",\"fullRootBounded\":"
         << (resources ? "true" : "false")
         << ",\"cachedWork\":" << cached.work
         << ",\"directWork\":" << direct.work << "}\n";
  return passed;
}

void writeAuditArtifact(const Options& options,
                        const ReplayResult& replay,
                        const AuditCounters& audit) {
  std::ofstream output(options.audit_output);
  if (!output) throw std::runtime_error("could not open cache-free audit");
  output << std::setprecision(12)
         << "{\"experiment\":\"d4-d2-rollout-veto-cache-free-audit\""
         << ",\"formalInference\":false,\"sameOpenSeedOnly\":true"
         << ",\"seed\":\"0x3ded0000\",\"freshSeedsOpened\":false"
         << ",\"score\":" << replay.game.score
         << ",\"moves\":" << replay.game.moves
         << ",\"switches\":" << replay.game.switches
         << ",\"routedDecisions\":" << replay.game.routed_decisions
         << ",\"D2CallsAudited\":" << audit.calls
         << ",\"exactForwardRootValuesAndActions\":"
         << audit.exact_forward
         << ",\"exactReflectedRootValuesAndActions\":"
         << audit.exact_reflections
         << ",\"reflectionEquivariantActions\":"
         << audit.reflection_equivariant_actions
         << ",\"symmetricTieExceptions\":"
         << audit.symmetric_tie_exceptions
         << ",\"reflectionEquivariantValues\":"
         << audit.reflection_equivariant_values
         << ",\"symmetricValueExceptions\":"
         << audit.symmetric_value_exceptions
         << ",\"cachedWork\":" << audit.cached_work
         << ",\"directWork\":" << audit.direct_work
         << ",\"cachedNodes\":" << audit.cached_nodes
         << ",\"directNodes\":" << audit.direct_nodes
         << ",\"auditDigest\":" << audit.digest
         << ",\"elapsedSeconds\":" << replay.game.elapsed_seconds
         << ",\"peakRssBytes\":" << replay.game.peak_rss_bytes << "}\n";
}

int auditPilot(const Options& options, std::ostream& report) {
  AuditCounters audit;
  active_audit = &audit;
  ReplayResult replay;
  try {
    replay = replayWith(&auditedCacheFreeDepthTwoAction, "cache-free-audit");
  } catch (...) {
    active_audit = nullptr;
    throw;
  }
  active_audit = nullptr;
  if (audit.calls != replay.game.compression.continuation.calls ||
      audit.exact_forward != audit.calls ||
      audit.exact_reflections != audit.calls) {
    throw std::runtime_error("cache-free audit did not cover every D2 call");
  }
  writeAuditArtifact(options, replay, audit);
  report << std::fixed << std::setprecision(6)
         << "D4_D2_CACHE_FREE_AUDIT {\"calls\":" << audit.calls
         << ",\"exactForward\":" << audit.exact_forward
         << ",\"exactReflections\":" << audit.exact_reflections
         << ",\"seconds\":" << replay.game.elapsed_seconds
         << ",\"artifact\":\"" << options.audit_output << "\"}\n";
  return 0;
}

void writeFinalArtifact(const Options& options,
                        const compressed::GameResult& game,
                        std::size_t trace_records) {
  const double projected =
      std::max(compressed::kFrozenBaselineSeconds, game.elapsed_seconds) *
      original::kFullProtocolProjectionWaves;
  std::ofstream output(options.output);
  if (!output) throw std::runtime_error("could not open cache-free artifact");
  output << std::setprecision(12)
         << "{\"experiment\":\"d4-d2-rollout-veto-cache-free\""
         << ",\"formalInference\":false,\"outcomePreserving\":true"
         << ",\"publicStateOnly\":true,\"seed\":\"0x3ded0000\""
         << ",\"uniqueSeedAlreadyOpen\":true,\"freshSeedsOpened\":false"
         << ",\"untouched\":[\"0x3ded0001...003\","
            "\"0x3dee0000...007\",\"0x3ebb...\",\"0x3ebc...\","
            "\"0x7d...\",\"0xd7...\"]"
         << ",\"compressedReference\":{\"path\":\""
         << kCompressedArtifact << "\",\"sha256\":\""
         << kCompressedArtifactSha256 << "\",\"seconds\":"
         << kCompressedSeconds << ",\"D2Calls\":" << kCompressedD2Calls
         << ",\"D2Work\":" << kCompressedD2Work << '}'
         << ",\"implementation\":{\"fullWidth\":true,"
            "\"continuationDepth\":2,\"genericInnerCache\":false,"
            "\"stringKeys\":false,\"LRU\":false,"
            "\"decisionActionMemoRetained\":true,"
            "\"exactSuffixReuseRetained\":true}"
         << ",\"frozenOutcomeParity\":{\"score\":" << game.score
         << ",\"moves\":" << game.moves
         << ",\"numberedCleared\":" << game.numbered_cleared
         << ",\"coversRevealed\":" << game.covers_revealed
         << ",\"routedDecisions\":" << game.routed_decisions
         << ",\"switches\":" << game.switches
         << ",\"passingAlternatives\":"
         << game.passing_alternatives
         << ",\"d4Work\":" << game.d4_work
         << ",\"matchedOriginal\":true}"
         << ",\"pilot\":{\"elapsedSeconds\":" << game.elapsed_seconds
         << ",\"d4Seconds\":" << game.d4_seconds
         << ",\"rolloutSeconds\":" << game.rollout_seconds
         << ",\"traceRecords\":" << trace_records
         << ",\"tracePath\":\"" << options.trace_output
         << "\",\"peakRssBytes\":" << game.peak_rss_bytes
         << ",\"compression\":";
  compressed::writeCompression(output, game.compression);
  output << "},\"runtimeProjection\":{\"firstPairSeconds\":"
         << std::max(compressed::kFrozenBaselineSeconds,
                     game.elapsed_seconds)
         << ",\"waves\":" << original::kFullProtocolProjectionWaves
         << ",\"projectedSeconds\":" << projected
         << ",\"limitSeconds\":"
         << original::kMaximumProjectedWallSeconds
         << ",\"fits\":"
         << (projected <= original::kMaximumProjectedWallSeconds
                 ? "true"
                 : "false")
         << "},\"remainingFittingAuthorized\":false"
         << ",\"approximationMenuStatus\":\"proposed-not-run\""
         << ",\"proposedApproximationMenu\":["
         << "{\"name\":\"h8-s7-d2\",\"horizon\":8,"
            "\"scenarios\":7,\"continuationDepth\":2},"
         << "{\"name\":\"h25-s7-d1\",\"horizon\":25,"
            "\"scenarios\":7,\"continuationDepth\":1}]}\n";
}

int replayPilot(const Options& options, std::ostream& report) {
  ReplayResult replay = replayWith(&cacheFreeDepthTwoAction,
                                   "cache-free-timing");
  std::ofstream trace(options.trace_output);
  if (!trace) throw std::runtime_error("could not open cache-free trace");
  trace << "{\"type\":\"metadata\","
        << "\"experiment\":\"d4-d2-rollout-veto-cache-free\","
        << "\"seed\":" << kPilotSeed
        << ",\"uniqueSeedAlreadyOpen\":true,\"records\":"
        << replay.records.size() << "}\n";
  for (std::size_t index = 0; index < replay.records.size(); ++index) {
    compressed::writeTraceRecord(trace, index, replay.records[index]);
  }
  trace << "{\"type\":\"summary\",\"score\":" << replay.game.score
        << ",\"moves\":" << replay.game.moves
        << ",\"switches\":" << replay.game.switches
        << ",\"elapsedSeconds\":" << replay.game.elapsed_seconds
        << ",\"compression\":";
  compressed::writeCompression(trace, replay.game.compression);
  trace << "}\n";
  trace.close();
  writeFinalArtifact(options, replay.game, replay.records.size());
  const double projected =
      std::max(compressed::kFrozenBaselineSeconds,
               replay.game.elapsed_seconds) *
      original::kFullProtocolProjectionWaves;
  report << std::fixed << std::setprecision(6)
         << "D4_D2_CACHE_FREE_REPLAY {\"score\":" << replay.game.score
         << ",\"moves\":" << replay.game.moves
         << ",\"switches\":" << replay.game.switches
         << ",\"seconds\":" << replay.game.elapsed_seconds
         << ",\"D2Calls\":"
         << replay.game.compression.continuation.calls
         << ",\"D2Work\":" << replay.game.compression.continuation.work
         << ",\"projectedSeconds\":" << projected
         << ",\"artifact\":\"" << options.output << "\"}\n";
  return projected <= original::kMaximumProjectedWallSeconds ? 0 : 3;
}

}  // namespace drop7::d4_d2_rollout_veto_cache_free

#ifndef DROP7_D4_D2_ROLLOUT_VETO_CACHE_FREE_LIBRARY
int main(int argc, char** argv) {
  try {
    using namespace drop7::d4_d2_rollout_veto_cache_free;
    const Options options = parseOptions(argc, argv, 2);
    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]) == "--audit-pilot") {
      return auditPilot(options, std::cout);
    }
    if (argc >= 2 && std::string_view(argv[1]) == "--replay-pilot") {
      return replayPilot(options, std::cout);
    }
    std::cerr << "usage: drop7_d4_d2_rollout_veto_cache_free "
                 "--self-test | --audit-pilot | --replay-pilot "
                 "[--output PATH] [--audit-output PATH] "
                 "[--trace-output PATH]\n";
    return 2;
  } catch (const std::exception& error) {
    std::cerr << "error: " << error.what() << '\n';
    return 1;
  }
}
#endif