Drop7 Research
approaches/lifetime-objective/fast-engine/gate-leaf.cppC++175 lines · 6.5 KB
// Bit-exact leaf gate.
//
// Compares drop7::fair_only_horizon::fairLeaf against drop7::fast::fastFairLeaf
// as raw uint64 bit patterns -- not as approximately equal doubles -- over the
// distribution of states the search actually evaluates, harvested from real
// games on the SEEDLEASE-A52-FAST block.  Also exhaustively verifies the two
// lookup tables that replaced libm calls.
//
// Any nonzero mismatch count means the fast leaf is a different evaluator and
// therefore a new algorithmic candidate, not an optimisation.

#include "slow-search.hpp"
#include "fast-leaf.hpp"
#include "corpus.hpp"

#include <cstring>
#include <iomanip>
#include <iostream>
#include <algorithm>
#include <vector>

namespace {

using namespace drop7;
using namespace drop7::fast;

std::uint64_t bits(double value) {
  std::uint64_t pattern = 0;
  std::memcpy(&pattern, &value, sizeof(pattern));
  return pattern;
}

bool checkWaveTable(std::ostream& out) {
  int mismatches = 0;
  for (int depth = 1; depth < kWaveTableSize; ++depth) {
    const std::int64_t original = drop7::scoreForWave(depth);
    const std::int64_t table = scoreForWaveFast(depth);
    if (original != table) ++mismatches;
  }
  // And beyond the table, where the fallback must still agree.
  for (int depth = kWaveTableSize; depth < kWaveTableSize + 64; ++depth) {
    if (drop7::scoreForWave(depth) != scoreForWaveFast(depth)) ++mismatches;
  }
  out << "wave-score table: depths 1.." << (kWaveTableSize + 63)
      << " checked against floor(7*pow(d,2.5)), " << mismatches
      << " mismatches\n";
  out << "  spot values d=1..6: ";
  for (int depth = 1; depth <= 6; ++depth) out << scoreForWaveFast(depth) << ' ';
  out << " d=20: " << scoreForWaveFast(20) << '\n';
  return mismatches == 0;
}

bool checkReadinessTable(std::ostream& out) {
  int mismatches = 0;
  for (int cost = -64; cost < kReadinessTableSize + 64; ++cost) {
    const double original = cfpi::detail::readiness(cost);
    const double fast = readinessFast(cost);
    if (bits(original) != bits(fast)) ++mismatches;
  }
  out << "readiness table: costs -64.." << (kReadinessTableSize + 63)
      << " bit-compared against ldexp(1.0, 1-cost), " << mismatches
      << " mismatches\n";
  return mismatches == 0;
}

}  // namespace

int main(int argc, char** argv) {
  int games = 24;
  int maximum_moves = 400;
  std::size_t leaf_target = 3'000'000;
  std::size_t per_root = 400;
  int strata = 5;
  for (int index = 1; index + 1 < argc; index += 2) {
    const std::string key = argv[index];
    if (key == "--games") games = std::stoi(argv[index + 1]);
    else if (key == "--max-moves") maximum_moves = std::stoi(argv[index + 1]);
    else if (key == "--leaves") leaf_target = std::stoull(argv[index + 1]);
    else if (key == "--per-root") per_root = std::stoull(argv[index + 1]);
    else if (key == "--strata") strata = std::stoi(argv[index + 1]);
  }

  static_assert(leafweights::kOpenColumnsWeight ==
                drop7::fair_only_horizon::kOpenColumnsWeight);
  static_assert(leafweights::kRisePressureWeight ==
                drop7::fair_only_horizon::kRisePressureWeight);
  static_assert(leafweights::kDangerHeightSquaredWeight ==
                drop7::fair_only_horizon::kDangerHeightSquaredWeight);
  static_assert(leafweights::kFairTerminalUtility ==
                drop7::fair_only_horizon::kFairTerminalUtility);

  std::cout << std::setprecision(17);
  bool ok = true;
  ok &= checkWaveTable(std::cout);
  ok &= checkReadinessTable(std::cout);

  // Real leaf states: play a real game with the frozen depth-4 policy, then
  // expand each root the way the search does and record the depth-0 states.
  std::vector<State> leaves;
  leaves.reserve(leaf_target);
  std::uint64_t roots = 0;
  for (int game = 0; game < games && leaves.size() < leaf_target; ++game) {
    const std::uint32_t seed =
        kLeafCorpusSeeds + static_cast<std::uint32_t>(game);
    requireLease(seed);
    std::vector<State> root_states;
    auto decide = [](const State& state) {
      return ref::chooseDepth4Action(state).action;
    };
    harvestRootStates(seed, maximum_moves, decide, root_states);
    for (const State& root : root_states) {
      if (leaves.size() >= leaf_target) break;
      ++roots;
      bool ignored = false;
      const State canonical = cfpi::detail::canonicalState(root, ignored);
      // Leaves occur at every iterative-deepening ply, so sample plies 1..4,
      // and cap each root so the corpus spans many real positions instead of
      // being one root's subtree.
      for (int ply = 1; ply <= 4; ++ply) {
        const std::size_t cap =
            std::min(leaf_target, leaves.size() + per_root);
        harvestSearchStates(canonical, ply, strata, 0,
                            drop7::fair_only_horizon::kPolicySeed, leaves, cap);
      }
    }
  }

  LeafScratch scratch;
  std::uint64_t mismatches = 0;
  std::uint64_t first_mismatch_index = 0;
  double first_original = 0.0;
  double first_fast = 0.0;
  std::uint64_t domain_violations = 0;
  for (std::size_t index = 0; index < leaves.size(); ++index) {
    const State& state = leaves[index];
    for (std::uint8_t cell : state.board) {
      if (cell > 15) ++domain_violations;
    }
    const double original = drop7::fair_only_horizon::fairLeaf(state);
    const double fast = fastFairLeaf(state, scratch);
    if (bits(original) != bits(fast)) {
      if (mismatches == 0) {
        first_mismatch_index = index;
        first_original = original;
        first_fast = fast;
      }
      ++mismatches;
    }
  }

  // Game-over states take the early return; check that path too.
  State terminal;
  terminal.board = initialBoard();
  terminal.game_over = true;
  const bool terminal_ok =
      bits(drop7::fair_only_horizon::fairLeaf(terminal)) ==
      bits(fastFairLeaf(terminal, scratch));

  std::cout << "leaf corpus: " << roots << " real roots expanded, "
            << leaves.size() << " leaf states compared\n";
  std::cout << "leaf bit-pattern mismatches: " << mismatches << '\n';
  std::cout << "cell-domain violations (cell > 15): " << domain_violations
            << '\n';
  std::cout << "terminal path identical: " << (terminal_ok ? "yes" : "no")
            << '\n';
  if (mismatches > 0) {
    std::cout << "  first mismatch at index " << first_mismatch_index
              << ": frozen " << first_original << " (0x" << std::hex
              << bits(first_original) << std::dec << ") fast " << first_fast
              << " (0x" << std::hex << bits(first_fast) << std::dec << ")\n";
  }
  ok &= mismatches == 0 && terminal_ok && domain_violations == 0;
  std::cout << (ok ? "LEAF GATE PASSED\n" : "LEAF GATE FAILED\n");
  return ok ? 0 : 1;
}