#pragma once
// Record layout for the fair-planner teacher corpus.
//
// WHY THIS FILE EXISTS
// --------------------
// `docs/exploratory/audit-05-optimistic-curriculum.md` classifies seventeen
// failed learned models in this repository. The single largest primary failure
// class is (iii) *sibling coverage / within-root discrimination*: six of the
// seventeen. Every one of those experiments labelled the played action, or too
// few actions, or labelled siblings with too much independent noise to separate
// them at one root.
//
// This corpus is built so that class cannot recur. At every decision the
// teacher records the exact value it computed for **every legal column**, under
// **common random numbers** (all columns share the same K sampled completions of
// the hidden board inside one decision), together with the realised afterstate
// of every legal column under the true environment. Nothing is left to a
// separate roll-out.
//
// The teacher is `approaches/lifetime-objective/flow-ceiling/fair-planner.hpp`
// arm B: legal by construction. `docs/exploratory/finding-07-fair-planning-
// ceiling.md` proves the information boundary with a self-test that swaps every
// hidden value and the entire future and requires an identical column. This is
// the first distillation target in the repository whose advantage is, by
// construction, representable from public state.
//
// One record per decision. Fixed width, packed, little-endian; the reader in
// `dataset.py` asserts the size.
#include <cstdint>
namespace drop7::distill {
constexpr int kCells = 49;
constexpr int kColumns = 7;
#pragma pack(push, 1)
struct RootRecord {
// ---- the public state the teacher decided from -------------------------
std::uint8_t board[kCells]; // 0 empty, 1..7 numbered, 8 solid, 9 cracked
std::uint8_t next_disc; // 1..7, visible
std::uint8_t moves_remaining; // 1..5 until the next rise
std::uint8_t legal_mask; // bit c set when column c is legal
std::uint8_t occupied; // occupied cells before the move, 0..49
// ---- what the teacher did ----------------------------------------------
std::uint8_t chosen_column; // the planner's argmax
std::uint8_t played_column; // what was actually played (differs iff explored)
std::uint8_t explored; // 1 when an epsilon deviation was played
std::uint8_t samples_used; // K completions that solved inside the budget
std::uint8_t incomplete; // completions that exceeded the window budget
// ---- the label: EVERY legal sibling, common random numbers --------------
// `value[c]` mean over the K completions of the exact horizon-H window
// optimum that starts by playing column c. Objective: numbered
// discs cleared. Illegal columns hold -1.
// `immediate[c]` mean over the same K completions of the discs cleared by the
// move itself. `value - immediate` is the mean value of the
// afterstate, which is the quantity a state-only afterstate
// evaluator can represent.
float value[kColumns];
float immediate[kColumns];
// The same value, computed from the first and second half of the K
// completions separately. `docs/benchmarks.md` requires "action stability
// across independent scenario halves" from a learned ranker; the same
// statistic on the TEACHER is the ceiling any student can reach, because a
// student cannot be more consistent than the label it is fitted to. Nothing
// in this repository has ever reported that ceiling.
float value_lo[kColumns];
float value_hi[kColumns];
// ---- the realised afterstate of every legal column ----------------------
// Resolved against the TRUE master tape, i.e. the same environment the game
// continues into, so siblings are compared under common random numbers in the
// environment as well as inside the planner. These boards are public: a
// revealed cover is a visible number.
std::uint8_t after_board[kColumns][kCells];
std::uint8_t after_survived[kColumns];
std::uint8_t after_clears[kColumns];
std::uint8_t after_reveals[kColumns];
std::uint8_t after_occupied[kColumns];
std::uint8_t after_next_disc[kColumns]; // the tape's next visible disc
std::uint8_t after_moves_remaining[kColumns];
// ---- provenance --------------------------------------------------------
std::uint16_t move_index; // 0-based within the game
std::uint16_t moves_to_end; // moves this game still had left (>=1)
std::uint8_t censored_game; // 1 when the source game hit the move cap
std::uint8_t horizon;
std::uint16_t samples_configured;
std::uint32_t game_seed;
std::uint8_t padding[9]; // keeps the record a round 576 bytes
};
#pragma pack(pop)
static_assert(sizeof(RootRecord) == 576, "RootRecord must stay 576 bytes");
} // namespace drop7::distill