Drop7 Research
approaches/lifetime-objective/chain-reveal-leaf/extra-terms.hppC++243 lines · 10.7 KB
#pragma once
// Extra leaf terms for the chain-reveal-leaf experiment
// (EX-20260823-reveal-construction-screen-371fd638), implementing
// runs/RUN-20260823T091530Z-cbe65468/kimi-k3-theory-design.md section 2
// (i)-(iii).  The definitions below are FROZEN by that experiment; change them
// only under a new experiment record.
//
// CONTRACT FOR EVERY TERM (the memo in augmented-leaf.hpp depends on it):
//   * public information only: the board and moves_remaining.  The signature
//     takes exactly those (no State), so score, level, moves_played and
//     next_disc reads are unrepresentable.  A term that needs the next disc must be
//     recomputed on a memo hit exactly as next_disc_vertical_options is in
//     fast-engine-memo/memo-leaf.hpp; the memo stores ExtraFeatures per
//     (board, moves_remaining) and would otherwise serve a stale value.
//     gate.cpp --leaf-bits checks memoised == fresh on every visited state.
//   * mirror-invariant: v on a board and on its horizontal mirror must be
//     equal.  gate.cpp --leaf-bits checks this on every visited state.
//   * finite on every reachable board.
//   * `scratch` is the LeafScratch that extractFastLeafFeatures<6> has just
//     filled for this board.  Read it; do not write it.  The per-cell arrays
//     addition[] / release[] are written only for NUMBERED cells (stages 2 and
//     3 of fast-leaf.hpp) and are read here only for numbered cells.
//
// QUANTITIES (all taken from the scratch the frozen leaf computed):
//   r[x]   = unionReadinessFast(scratch.addition[x], scratch.release[x])
//            -- the per-disc "about to pop" readiness the frozen leaf uses for
//            cover exposure (fast-leaf.hpp stage 6).
//   rel[x] = scratch.release[x]
//            -- the RELEASE readiness alone (the per-disc summand of
//            latent_chain_potential, fast-leaf.hpp stage 3): the disc pops
//            because run-mates leave, i.e. the hit arrives as a wave.
//   heights, run table: scratch.heights / scratch.horizontal(row).
//
// SUPPORT-DISJOINT RULE for a pair {a,b} of orthogonal numbered neighbours of
// a cover g (used by aligned_double_hit and chain_to_crack_solid).  A pair is
// SKIPPED when either of these holds:
//   R1 (opposite sides: a and b are both in g's column or both in g's row, so
//       they lie in one contiguous occupied run through g): if a's readiness
//       is release-dominated (release[a] > addition[a]) then a's completion
//       needs a run-mate -- possibly b -- to leave first, which is a different
//       wave; skip.  Symmetrically for b.
//   R2 (adjacent sides: one of a,b is in g's column and the other in g's
//       row): skip if the completion paths P(a) and P(b) share a cell, where
//       P(x) = V(x) | H(x):
//         V(x) = the empty cells of x's column that vertical completion would
//                fill: rows [7 - value, 7 - height - 1] when value > height,
//                else empty;
//         H(x) = the union, over every minimal-cost horizontal window for x
//                (the same windows minimumHorizontalAdditionCostFast scans,
//                with the same edge-blocking and the same cost), of the empty
//                cells each window column must fill to reach x's row; empty
//                when no window exists.
//       Taking the union over all minimal windows (instead of the first) is
//       what makes the rule mirror-invariant.
// The rule is deterministic, reads only the board, and is symmetric in a,b.
//
// danger_gate = 1.0 if max column height <= 4, 0.5 if it is 5, 0.0 if >= 6
// (Kimi's danger = max(0, max_height - 4): 0 -> 1, 1 -> 0.5, >= 2 -> 0).

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

#include <algorithm>
#include <cstdint>
#include <limits>

namespace drop7::fastx {

constexpr int kExtraTerms = 7;
constexpr const char* kExtraNames[kExtraTerms] = {
    "aligned_double_hit",          // (i)  sum over kSolid g of max pair r[a]*r[b]
    "chain_to_crack_cracked",      // (ii) sum over kCracked g of noisy-OR rel[d]
    "chain_to_crack_solid",        // (ii) sum over kSolid g of max pair rel[a]*rel[b]
    "entombed_high",               // (iii) entombed 3..7 weighted by (1 - r)
    "aligned_double_hit_gated",    // (i)  x danger_gate
    "chain_to_crack_cracked_gated",// (ii) x danger_gate
    "chain_to_crack_solid_gated",  // (ii) x danger_gate
};

struct ExtraFeatures {
  double v[kExtraTerms] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
};

namespace detail {

inline std::uint64_t bit(int row, int column) {
  return 1ull << (row * kBoardSize + column);
}

// Completion path P(x) of the numbered cell at `index` (see header).
inline std::uint64_t completionPath(const Board& board,
                                    const fast::LeafScratch& scratch,
                                    int index) {
  const int row = index / kBoardSize;
  const int column = index % kBoardSize;
  const int value = static_cast<int>(board[static_cast<std::size_t>(index)]);
  const auto& heights = scratch.heights;
  std::uint64_t path = 0;
  // V(x)
  const int height = heights[static_cast<std::size_t>(column)];
  if (value > height) {
    for (int r = kBoardSize - value; r <= kBoardSize - height - 1; ++r) {
      path |= bit(r, column);
    }
  }
  // H(x): same window enumeration as minimumHorizontalAdditionCostFast.
  const int elevation = kBoardSize - row;
  const fast::RunInfo& horizontal = scratch.horizontal(row);
  const int segment_start = horizontal.start[column];
  const int segment_end = horizontal.end[column];
  const int segment_length = horizontal.length[column];
  if (segment_start < 0 || value <= segment_length) return path;
  int best = std::numeric_limits<int>::max();
  std::uint64_t union_of_best = 0;
  const int lowest = std::max(0, segment_end - value + 1);
  const int highest = std::min(segment_start, kBoardSize - value);
  for (int start = lowest; start <= highest; ++start) {
    const int end = start + value - 1;
    if (start > 0 && heights[static_cast<std::size_t>(start - 1)] >= elevation) continue;
    if (end + 1 < kBoardSize && heights[static_cast<std::size_t>(end + 1)] >= elevation) continue;
    int cost = 0;
    std::uint64_t cells = 0;
    for (int c = start; c <= end; ++c) {
      const int h = heights[static_cast<std::size_t>(c)];
      if (h >= elevation) continue;
      cost += elevation - h;
      for (int r = row; r <= kBoardSize - h - 1; ++r) cells |= bit(r, c);
    }
    if (cost <= 0) continue;
    if (cost < best) {
      best = cost;
      union_of_best = cells;
    } else if (cost == best) {
      union_of_best |= cells;
    }
  }
  return path | union_of_best;
}

// Neighbour slots of g in the fixed order up, down, left, right; -1 if absent
// or not numbered.  Sides 0,1 are g's column; 2,3 are g's row.
inline void numberedNeighbours(const fast::LeafScratch& scratch, int index,
                               int out[4]) {
  const int row = index / kBoardSize;
  const int column = index % kBoardSize;
  const auto slot = static_cast<std::size_t>(index);
  out[0] = row > 0 && scratch.present(slot - kBoardSize) ? index - kBoardSize : -1;
  out[1] = row + 1 < kBoardSize && scratch.present(slot + kBoardSize) ? index + kBoardSize : -1;
  out[2] = column > 0 && scratch.present(slot - 1) ? index - 1 : -1;
  out[3] = column + 1 < kBoardSize && scratch.present(slot + 1) ? index + 1 : -1;
}

// True when the pair (side i, side j) of g is support-disjoint (see header).
inline bool supportDisjoint(const Board& board, const fast::LeafScratch& scratch,
                            int i, int j, int a, int b, std::uint64_t path[4]) {
  const bool sameAxis = (i < 2) == (j < 2);
  if (sameAxis) {  // R1
    const auto sa = static_cast<std::size_t>(a), sb = static_cast<std::size_t>(b);
    if (scratch.release[sa] > scratch.addition[sa]) return false;
    if (scratch.release[sb] > scratch.addition[sb]) return false;
    return true;
  }
  // R2
  if (path[i] == ~0ull) path[i] = completionPath(board, scratch, a);
  if (path[j] == ~0ull) path[j] = completionPath(board, scratch, b);
  return (path[i] & path[j]) == 0;
}

// max over support-disjoint neighbour pairs of q[a]*q[b], q = r or rel.
template <typename Quantity>
inline double bestPair(const Board& board, const fast::LeafScratch& scratch,
                       int index, Quantity q) {
  int n[4];
  numberedNeighbours(scratch, index, n);
  std::uint64_t path[4] = {~0ull, ~0ull, ~0ull, ~0ull};
  double best = 0.0;
  for (int i = 0; i < 4; ++i) {
    if (n[i] < 0) continue;
    for (int j = i + 1; j < 4; ++j) {
      if (n[j] < 0) continue;
      if (!supportDisjoint(board, scratch, i, j, n[i], n[j], path)) continue;
      best = std::max(best, q(n[i]) * q(n[j]));
    }
  }
  return best;
}

}  // namespace detail

inline void extractExtraFeatures(const Board& board,
                                 [[maybe_unused]] int moves_remaining,
                                 const fast::LeafScratch& scratch,
                                 ExtraFeatures& out) {
  auto r = [&](int x) {
    const auto s = static_cast<std::size_t>(x);
    return fast::unionReadinessFast(scratch.addition[s], scratch.release[s]);
  };
  auto rel = [&](int x) { return scratch.release[static_cast<std::size_t>(x)]; };

  double aligned = 0.0, crackedChain = 0.0, solidChain = 0.0, entombed = 0.0;
  for (std::uint64_t remaining = scratch.cover_bits; remaining != 0;
       remaining &= remaining - 1) {
    const int index = __builtin_ctzll(remaining);
    if (board[static_cast<std::size_t>(index)] == kSolid) {
      aligned += detail::bestPair(board, scratch, index, r);
      solidChain += detail::bestPair(board, scratch, index, rel);
    } else {
      int n[4];
      detail::numberedNeighbours(scratch, index, n);
      double inverse = 1.0;
      for (int k = 0; k < 4; ++k) {
        if (n[k] >= 0) inverse *= 1.0 - rel(n[k]);
      }
      crackedChain += 1.0 - inverse;
    }
  }
  for (std::uint64_t remaining = scratch.present_bits; remaining != 0;
       remaining &= remaining - 1) {
    const int index = __builtin_ctzll(remaining);
    const int value = static_cast<int>(board[static_cast<std::size_t>(index)]);
    if (value < 3) continue;
    const int row = index / kBoardSize;
    const int column = index % kBoardSize;
    if (scratch.heights[static_cast<std::size_t>(column)] > value &&
        scratch.horizontal(row).length[column] > value) {
      entombed += 1.0 - r(index);
    }
  }
  int maximumHeight = 0;
  for (int column = 0; column < kBoardSize; ++column) {
    maximumHeight = std::max(maximumHeight, scratch.heights[static_cast<std::size_t>(column)]);
  }
  const double gate = maximumHeight <= 4 ? 1.0 : maximumHeight == 5 ? 0.5 : 0.0;

  out.v[0] = aligned;
  out.v[1] = crackedChain;
  out.v[2] = solidChain;
  out.v[3] = entombed;
  out.v[4] = aligned * gate;
  out.v[5] = crackedChain * gate;
  out.v[6] = solidChain * gate;
}

}  // namespace drop7::fastx