Drop7 Research
approaches/lifetime-objective/chain-reveal-leaf/augmented-leaf.hppC++167 lines · 6.6 KB
#pragma once
// The frozen fair leaf plus K weighted extra terms, behind the one-entry
// board memo of approaches/lifetime-objective/fast-engine-memo/memo-leaf.hpp.
//
// EQUIVALENCE CONTRACT.  augmentedFairLeaf computes `result` exactly as
// fastm::fastFairLeafMemo does -- the same feature extraction, the same
// memo key (board, moves_remaining), the same next_disc_vertical_options
// recomputation from stored heights, the same dot product in the frozen
// order with the frozen constants -- and only THEN adds `w[i] * v[i]` for
// every extra term whose weight is not exactly zero.  A zero-weight term is
// skipped, not multiplied: `result + 0.0 * v` is the identity on finite
// inputs, but the contract does not rely on that argument, it relies on the
// instruction never being emitted.  At all-zero weights the returned double is
// therefore the same uint64 bit pattern as fast::fastFairLeaf on every board,
// which gate.cpp --leaf-bits checks on the boards a real search visits.
//
// The memo adds no state the search reads; it sits below the search's ++work_
// (build.sh checks that), so logical work, completed depth and cache
// behaviour are unchanged whatever the weights are.
//
// This header modifies no existing repository file.

#include "../fast-engine/fast-leaf.hpp"
#include "extra-terms.hpp"

#include <cmath>
#include <cstdint>
#include <cstring>
#include <sstream>
#include <stdexcept>
#include <string>

namespace drop7::fastx {

struct ExtraWeights {
  double w[kExtraTerms] = {};  // all zero: the frozen arm

  bool isFrozen() const {
    for (int i = 0; i < kExtraTerms; ++i) {
      if (w[i] != 0.0) return false;
    }
    return true;
  }

  // "name=value,name=value"; unnamed terms stay zero; an empty string is the
  // frozen arm.  Unknown names and non-finite values are errors so that a typo
  // cannot silently mean "frozen".
  static ExtraWeights parse(const std::string& text) {
    ExtraWeights weights;
    std::size_t cursor = 0;
    while (cursor < text.size()) {
      const std::size_t comma = text.find(',', cursor);
      const std::string item =
          text.substr(cursor, comma == std::string::npos ? std::string::npos
                                                         : comma - cursor);
      cursor = comma == std::string::npos ? text.size() : comma + 1;
      if (item.empty()) continue;
      const std::size_t equals = item.find('=');
      if (equals == std::string::npos) {
        throw std::invalid_argument("extra weight without '=': " + item);
      }
      const std::string name = item.substr(0, equals);
      const int index = indexOf(name);
      if (index < 0) throw std::invalid_argument("unknown extra term " + name);
      std::size_t consumed = 0;
      const double value = std::stod(item.substr(equals + 1), &consumed);
      if (consumed != item.size() - equals - 1 || !std::isfinite(value)) {
        throw std::invalid_argument("bad value for extra term " + name);
      }
      weights.w[index] = value;
    }
    return weights;
  }

  static int indexOf(const std::string& name) {
    for (int i = 0; i < kExtraTerms; ++i) {
      if (name == kExtraNames[i]) return i;
    }
    return -1;
  }

  // Canonical "name=value,..." over every term, in term order, for artifacts.
  std::string describe() const {
    std::ostringstream out;
    out.precision(17);
    for (int i = 0; i < kExtraTerms; ++i) {
      if (i) out << ',';
      out << kExtraNames[i] << '=' << w[i];
    }
    return out.str();
  }
};

// memo-leaf.hpp's LeafMemo plus the extra features of the same board.
struct AugmentedMemo {
  bool valid = false;
  Board board{};
  int moves_remaining = 0;
  std::array<int, kBoardSize> heights{};
  fast::FastLeafFeatures features;
  ExtraFeatures extra;
  std::uint64_t calls = 0;
  std::uint64_t hits = 0;
};

inline double augmentedFairLeaf(const State& state, fast::LeafScratch& scratch,
                                AugmentedMemo& memo,
                                const ExtraWeights& weights) {
  using namespace fast::leafweights;
  if (state.game_over) return kFairTerminalUtility;
  if (state.moves_remaining < 1 || state.moves_remaining > kMovesPerLevel ||
      state.next_disc < 1 || state.next_disc > kBoardSize) {
    throw std::invalid_argument("invalid public state for fair evaluator");
  }
  ++memo.calls;
  if (!memo.valid || memo.moves_remaining != state.moves_remaining ||
      std::memcmp(memo.board.data(), state.board.data(), kCellCount) != 0) {
    fast::FastLeafFeatures fresh;
    fast::extractFastLeafFeatures<6>(state, scratch, fresh);
    memo.features = fresh;
    extractExtraFeatures(state.board, state.moves_remaining, scratch, memo.extra);
    memo.board = state.board;
    memo.moves_remaining = state.moves_remaining;
    memo.heights = scratch.heights;
    memo.valid = true;
  } else {
    ++memo.hits;
  }
  fast::FastLeafFeatures f = memo.features;
  // fast-leaf.hpp:271-275, same column order and its own accumulator.
  f.next_disc_vertical_options = 0;
  for (int column = 0; column < kBoardSize; ++column) {
    const int height = memo.heights[static_cast<std::size_t>(column)];
    if (height < kBoardSize && height + 1 == state.next_disc) {
      f.next_disc_vertical_options += 1.0;
    }
  }
  // fast-leaf.hpp:536-561, the frozen dot-product order.
  double result = 0.0;
  result += kOpenColumnsWeight * f.open_columns;
  result += kHeightLoadWeight * f.height_load;
  result += kSolidCellsWeight * f.solid_cells;
  result += kCrackedCellsWeight * f.cracked_cells;
  result += kNumberedCellsWeight * f.numbered_cells;
  result += kHighLowNumbersWeight * f.high_low_numbers;
  result += kDirectPotentialWeight * f.direct_potential;
  result += kLatentChainPotentialWeight * f.latent_chain_potential;
  result += kCrackedExposureWeight * f.cracked_exposure;
  result += kSolidExposureWeight * f.solid_exposure;
  result += kAdjacentOnesWeight * f.adjacent_ones;
  result += kTripleTwosWeight * f.triple_twos;
  result += kDeadLowNumbersWeight * f.dead_low_numbers;
  result += kCoveredHeightRiskWeight * f.covered_height_risk;
  result += kLowNumberHeightRiskWeight * f.low_number_height_risk;
  result += kDangerHeightSquaredWeight * f.danger_height_squared;
  result += kRisePressureWeight * f.rise_pressure;
  result += kNextDiscVerticalOptionsWeight * f.next_disc_vertical_options;
  // Extra terms, after the frozen sum and only when the weight is non-zero,
  // so the zero-weight arm emits no floating-point instruction past this
  // line.  Term order is the kExtraNames order.
  for (int i = 0; i < kExtraTerms; ++i) {
    if (weights.w[i] != 0.0) result += weights.w[i] * memo.extra.v[i];
  }
  return result;
}

}  // namespace drop7::fastx