// Stage D0 of the H-pool program (EX-20260823-hpool-stage-d0-e0ad1c65):
// fair relabelling of oracle-visited public states against matched fair-D4
// public states.
//
// One source, two binaries:
//
// d0-generate (compiled with -DD0_GENERATE) runs the privileged oracle
// games and the public fair-D4 games, samples states from
// move 50 onward, matches F to O on (rise phase, occupancy
// bin, height bin) exactly as oracle-topology-audit.cpp bins
// them, records the realised remaining moves on each state's
// own trajectory (R_tape / R_real) and fair D4's column at
// every O root, and writes public records. Oracle privilege
// terminates at stripToPublic(); nothing after it can read the
// seed or the tape.
//
// d0-relabel (compiled without D0_GENERATE) links no oracle code at all
// (gate.sh checks the symbol table). It reads the public
// tuple (board, next disc, moves until rise) from each record
// and computes R_fair: the mean remaining moves over K = 32
// public futures derived from the public-state hash through
// the domain-separated restart streams of
// oracle-curriculum.cpp ("CRRV" reveals, "CRVS" visible discs),
// under fair depth-1 continuation at horizon 25, plus the same
// quantity for every legal first column under the same 32
// futures (common random numbers), the flow band, and the
// per-scenario censor flags. Label fields in the input
// (columns, realised moves) are copied through untouched and
// never parsed.
//
// The restart machinery is the one in
// approaches/oracle-curriculum/state-curriculum/oracle-curriculum.cpp with
// two deliberate differences, both recorded in README.mdx: K = 32 scenarios
// instead of 7, and the restart is played in the canonical orientation at
// every step so R_fair is exactly mirror-invariant (the original played in
// the source orientation, where row-major reveal order breaks exactness).
//
// Compile: see build.sh (clang++ -O3 -std=c++20 -pthread -Wall -Wextra
// -Werror -ffp-contract=off).
#ifdef D0_GENERATE
#define DROP7_ORACLE_TOPOLOGY_LIBRARY
#include "../topology/oracle-topology-audit.cpp"
#undef DROP7_ORACLE_TOPOLOGY_LIBRARY
#endif
#define DROP7_FAIR_ONLY_DEPTH4_LIBRARY
#include "../../fair-expectimax/reference/fair-only-depth4.cpp"
#undef DROP7_FAIR_ONLY_DEPTH4_LIBRARY
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <map>
#include <mutex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <sys/resource.h>
#include <type_traits>
#include <vector>
namespace drop7::hpool_d0 {
namespace fair = drop7::fair_only_horizon;
namespace d4 = drop7::fair_only_depth4;
using Clock = std::chrono::steady_clock;
// ---- frozen constants (EX-20260823-hpool-stage-d0-e0ad1c65) ----------------
constexpr std::uint32_t kOracleSeedStart = 0xa52e'0000u;
constexpr int kOracleGames = 64;
constexpr std::uint32_t kFairSeedStart = 0xa52e'0100u;
constexpr int kFairSeeds = 256;
[[maybe_unused]] constexpr std::uint32_t kProbeSeedStart = 0xa527'8000u; // already opened
[[maybe_unused]] constexpr std::uint32_t kProbeSeedEnd = 0xa527'8500u; // exclusive
[[maybe_unused]] constexpr int kOracleDepth = 4;
[[maybe_unused]] constexpr int kOracleBeam = 128;
[[maybe_unused]] constexpr int kMaximumMoves = 500;
[[maybe_unused]] constexpr int kFirstSampleMove = 50;
constexpr int kTargetStates = 2'000;
constexpr int kPerGameQuota = kTargetStates / kOracleGames; // 31 -> 1,984
constexpr int kScenarios = 32;
constexpr int kHorizon = 25;
constexpr int kEventsPerStep = 64;
[[maybe_unused]] constexpr int kFairBatch = 16; // seeds per matching round, fixed
[[maybe_unused]] constexpr int kPerGameBucketCap = 2; // F states per bucket per F game
constexpr int kOccupancyBinWidth = 4; // oracle-topology-audit.cpp
constexpr int kHeightBinWidth = 2; // oracle-topology-audit.cpp
constexpr int kMaximumThreads = 16;
constexpr double kWallLimitSeconds = 7'200.0;
constexpr std::uint64_t kRssLimitBytes = 8ull * 1024ull * 1024ull * 1024ull;
constexpr std::uint64_t kRestartSeedDomain = 0x4355'5252'5345'4544ull;
constexpr std::uint32_t kRestartRevealDomain = 0x4352'5256u; // "CRRV"
constexpr std::uint32_t kRestartVisibleDomain = 0x4352'5653u; // "CRVS"
static_assert(kLevelBonus == 17'000 && kMovesPerLevel == 5);
static_assert(kOracleSeedStart + kOracleGames <= kFairSeedStart);
static_assert(kFairSeedStart + kFairSeeds == 0xa52e'0200u);
static_assert(kPerGameQuota * kOracleGames <= kTargetStates);
static_assert(kEventsPerStep > kCellCount);
static_assert(kRestartRevealDomain != kRestartVisibleDomain);
static_assert(kRestartRevealDomain != kRevealDomain &&
kRestartRevealDomain != kNextDiscDomain &&
kRestartRevealDomain != cfpi::detail::kRevealSampleDomain &&
kRestartRevealDomain != cfpi::detail::kDiscSampleDomain);
static_assert(kRestartVisibleDomain != kRevealDomain &&
kRestartVisibleDomain != kNextDiscDomain &&
kRestartVisibleDomain != cfpi::detail::kRevealSampleDomain &&
kRestartVisibleDomain != cfpi::detail::kDiscSampleDomain);
static_assert(fair::kChanceSamples == 5);
#ifdef D0_GENERATE
static_assert(kOracleDepth == drop7::oracle_topology::kDefaultOracleDepth);
static_assert(kOracleBeam == drop7::oracle_topology::kDefaultOracleBeam);
static_assert(kOccupancyBinWidth == drop7::oracle_topology::kOccupancyBinWidth);
static_assert(kHeightBinWidth == drop7::oracle_topology::kHeightBinWidth);
#endif
std::mutex progress_mutex;
// ---- public state: the privilege boundary ----------------------------------
struct PublicState {
Board board{};
std::uint8_t next_disc = 1;
std::uint8_t moves_remaining = kMovesPerLevel;
bool operator==(const PublicState&) const = default;
};
PublicState stripToPublic(const State& source) {
if (source.game_over || source.next_disc < 1 ||
source.next_disc > kBoardSize || source.moves_remaining < 1 ||
source.moves_remaining > kMovesPerLevel) {
throw std::invalid_argument("source is not restartable");
}
for (const std::uint8_t cell : source.board) {
if (cell > kCracked) throw std::invalid_argument("invalid board token");
}
return {source.board, source.next_disc,
static_cast<std::uint8_t>(source.moves_remaining)};
}
State materialize(const PublicState& source) {
State result;
result.board = source.board;
result.next_disc = source.next_disc;
result.score = 0;
result.level = 1;
result.moves_remaining = source.moves_remaining;
result.moves_played = 0;
result.game_over = false;
return result;
}
PublicState canonicalPublic(const PublicState& source, bool& mirrored) {
const State canonical =
cfpi::detail::canonicalState(materialize(source), mirrored);
return stripToPublic(canonical);
}
PublicState mirror(const PublicState& source) {
PublicState result = source;
result.board = cfpi::detail::mirrorBoard(source.board);
return result;
}
std::uint64_t mix64(std::uint64_t value) {
value ^= value >> 30u;
value *= 0xbf58'476d'1ce4'e5b9ull;
value ^= value >> 27u;
value *= 0x94d0'49bb'1331'11ebull;
return value ^ (value >> 31u);
}
std::uint64_t publicHash(const PublicState& source) {
bool ignored = false;
const PublicState state = canonicalPublic(source, ignored);
std::uint64_t hash = 0xcbf2'9ce4'8422'2325ull;
for (const std::uint8_t cell : state.board) {
hash ^= static_cast<std::uint64_t>(cell + 1u);
hash *= 0x0000'0100'0000'01b3ull;
}
hash ^= state.next_disc;
hash *= 0x0000'0100'0000'01b3ull;
hash ^= static_cast<std::uint64_t>(state.moves_remaining + 1u);
return mix64(hash);
}
std::uint32_t seed32(std::uint64_t value) {
return mix32(static_cast<std::uint32_t>(value) ^
static_cast<std::uint32_t>(value >> 32u));
}
std::uint32_t restartSeed(const PublicState& source) {
return seed32(publicHash(source) ^ kRestartSeedDomain);
}
struct Bucket {
int rise_phase = 0;
int occupancy_bin = 0;
int height_bin = 0;
auto operator<=>(const Bucket&) const = default;
};
struct Shape {
int occupancy = 0;
int maximum_height = 0;
int legal_columns = 0;
};
Shape shapeOf(const PublicState& state) {
Shape result;
const auto heights = cfpi::detail::columnHeights(state.board);
for (int column = 0; column < kBoardSize; ++column) {
result.occupancy += heights[column];
result.maximum_height = std::max(result.maximum_height, heights[column]);
result.legal_columns += heights[column] < kBoardSize;
}
return result;
}
Bucket bucketOf(const PublicState& state) {
const Shape shape = shapeOf(state);
return {state.moves_remaining, shape.occupancy / kOccupancyBinWidth,
shape.maximum_height / kHeightBinWidth};
}
std::uint64_t peakRssBytes() {
rusage usage{};
if (getrusage(RUSAGE_SELF, &usage) != 0) return 0;
return static_cast<std::uint64_t>(usage.ru_maxrss) * 1024ull;
}
void enforceRssLimit() {
if (peakRssBytes() > kRssLimitBytes) {
throw std::runtime_error("hpool-d0 exceeded the 8 GiB RSS cap");
}
}
struct Deadline {
Clock::time_point started = Clock::now();
double limit = kWallLimitSeconds;
double elapsedSeconds() const {
return std::chrono::duration<double>(Clock::now() - started).count();
}
bool expired() const { return elapsedSeconds() > limit; }
};
// ---- public restart streams (oracle-curriculum.cpp, K = 32) ----------------
struct RestartRandom {
std::uint32_t root_seed = 0;
int scenario = 0;
int step = 0;
int event = 0;
std::uint8_t nextDisc() {
if (event >= kEventsPerStep) {
throw std::runtime_error("restart reveal event slice exhausted");
}
const int event_index = step * kEventsPerStep + event++;
const double unit = cfpi::detail::stratifiedUnit(
root_seed, scenario, kScenarios, kRestartRevealDomain, event_index);
return static_cast<std::uint8_t>(
std::floor(unit * static_cast<double>(kBoardSize)) + 1.0);
}
};
std::uint8_t restartVisibleDisc(std::uint32_t root_seed, int scenario,
int step) {
const double unit = cfpi::detail::stratifiedUnit(
root_seed, scenario, kScenarios, kRestartVisibleDomain, step);
return static_cast<std::uint8_t>(
std::floor(unit * static_cast<double>(kBoardSize)) + 1.0);
}
bool playRestartMove(const PublicState& source, int action,
std::uint32_t root_seed, int scenario, int step,
MoveResult& result) {
if (scenario < 0 || scenario >= kScenarios || step < 0 ||
step >= kHorizon || !isLegal(source.board, action)) {
return false;
}
const State state = materialize(source);
RestartRandom random{root_seed, scenario, step, 0};
if (!cfpi::detail::playMoveSampled(state, action, random, result)) {
return false;
}
result.state.score = 0;
result.state.level = 1;
result.state.moves_played = 0;
if (!result.state.game_over) {
result.state.next_disc = restartVisibleDisc(root_seed, scenario, step);
}
return true;
}
// Fair depth-1 continuation on a canonical public state. Returns the action
// in the canonical frame.
int chooseFairD1Canonical(const PublicState& canonical) {
fair::SearchContext context;
const fair::RootEvaluation root =
fair::rootDecision(materialize(canonical), 1, context);
int legal = 0;
int evaluated = 0;
for (int action = 0; action < kBoardSize; ++action) {
legal += isLegal(canonical.board, action);
evaluated += std::isfinite(root.values[action]);
}
if (root.action < 0 || legal != evaluated || context.work > 70 ||
!context.cache.empty()) {
throw std::runtime_error("restart fair D1 failed exact completion");
}
return root.action;
}
struct RestartOutcome {
int moves = 0;
int clears = 0;
int reveals = 0;
bool survived_horizon = false;
bool operator==(const RestartOutcome&) const = default;
};
// One public future. forced_first is a column in the ROOT's own frame
// (-1 = let D1 choose). The restart is played in the canonical frame at
// every step, so the outcome of a state and of its mirror are identical.
RestartOutcome runRestart(const PublicState& root, int scenario,
int forced_first = -1) {
if (scenario < 0 || scenario >= kScenarios) {
throw std::invalid_argument("invalid restart scenario");
}
bool mirrored = false;
PublicState state = canonicalPublic(root, mirrored);
const std::uint32_t root_seed = restartSeed(root);
RestartOutcome result;
for (int step = 0; step < kHorizon; ++step) {
int action = -1;
if (step == 0 && forced_first >= 0) {
action = mirrored ? kBoardSize - 1 - forced_first : forced_first;
} else {
action = chooseFairD1Canonical(state);
}
if (!isLegal(state.board, action)) {
throw std::runtime_error("restart chose an illegal action");
}
MoveResult move;
if (!playRestartMove(state, action, root_seed, scenario, step, move)) {
throw std::runtime_error("restart transition failed");
}
++result.moves;
for (const Wave& wave : move.waves) {
result.clears += wave.cleared;
result.reveals += wave.revealed;
}
if (move.state.game_over) return result;
bool ignored = false;
state = canonicalPublic(stripToPublic(move.state), ignored);
}
result.survived_horizon = true;
return result;
}
enum class FlowBand { kBlocked, kClosed, kRecovering, kFlowing };
std::string_view flowBandName(FlowBand band) {
switch (band) {
case FlowBand::kBlocked: return "blocked";
case FlowBand::kClosed: return "closed";
case FlowBand::kRecovering: return "recovering";
case FlowBand::kFlowing: return "flowing";
}
throw std::logic_error("unknown flow band");
}
struct Relabel {
double mean_moves = 0.0;
int survived = 0;
double reveals_per_move = 0.0;
double clears_per_move = 0.0;
FlowBand flow = FlowBand::kBlocked;
std::array<int, kScenarios> scenario_moves{};
std::array<double, kBoardSize> sibling_mean_moves{}; // NaN if illegal
std::array<int, kBoardSize> sibling_survived{};
bool operator==(const Relabel&) const = default;
};
// The relabel path: a function of the public tuple only.
Relabel relabel(const PublicState& state) {
Relabel result;
int total_moves = 0;
int total_clears = 0;
int total_reveals = 0;
for (int scenario = 0; scenario < kScenarios; ++scenario) {
const RestartOutcome outcome = runRestart(state, scenario);
result.scenario_moves[scenario] = outcome.moves;
result.survived += outcome.survived_horizon;
total_moves += outcome.moves;
total_clears += outcome.clears;
total_reveals += outcome.reveals;
}
result.mean_moves = static_cast<double>(total_moves) / kScenarios;
if (total_moves > 0) {
result.reveals_per_move =
static_cast<double>(total_reveals) / total_moves;
result.clears_per_move = static_cast<double>(total_clears) / total_moves;
}
const double survival_rate =
static_cast<double>(result.survived) / kScenarios;
if (survival_rate < 0.25) {
result.flow = FlowBand::kBlocked;
} else if (result.reveals_per_move < 0.25) {
result.flow = FlowBand::kClosed;
} else if (result.reveals_per_move < 0.60) {
result.flow = FlowBand::kRecovering;
} else {
result.flow = FlowBand::kFlowing;
}
for (int column = 0; column < kBoardSize; ++column) {
if (!isLegal(state.board, column)) {
result.sibling_mean_moves[column] =
std::numeric_limits<double>::quiet_NaN();
result.sibling_survived[column] = -1;
continue;
}
int moves = 0;
int survived = 0;
for (int scenario = 0; scenario < kScenarios; ++scenario) {
const RestartOutcome outcome = runRestart(state, scenario, column);
moves += outcome.moves;
survived += outcome.survived_horizon;
}
result.sibling_mean_moves[column] =
static_cast<double>(moves) / kScenarios;
result.sibling_survived[column] = survived;
}
return result;
}
using RelabelFunction = Relabel (*)(const PublicState&);
static_assert(std::is_same_v<decltype(&relabel), RelabelFunction>);
static_assert(!std::is_invocable_v<RelabelFunction, const State&>);
static_assert(!std::is_invocable_v<RelabelFunction, std::uint32_t>);
// ---- JSON helpers -----------------------------------------------------------
std::string hexSeed(std::uint32_t seed) {
std::ostringstream output;
output << "0x" << std::hex << std::setw(8) << std::setfill('0') << seed;
return output.str();
}
std::string hex64(std::uint64_t value) {
std::ostringstream output;
output << "0x" << std::hex << std::setw(16) << std::setfill('0') << value;
return output.str();
}
std::string numberText(double value) {
if (std::isnan(value)) return "null";
std::ostringstream output;
output << std::setprecision(17) << value;
return output.str();
}
void writePublicState(std::ostream& output, const PublicState& state) {
output << "{\"board\":\"" << serializeBoard(state.board)
<< "\",\"nextDisc\":" << static_cast<int>(state.next_disc)
<< ",\"movesRemaining\":" << static_cast<int>(state.moves_remaining)
<< '}';
}
// Minimal extraction of the public tuple from one JSONL record. Only these
// three fields are ever parsed by the relabel path.
PublicState parsePublicState(const std::string& line) {
const auto field = [&](std::string_view name) -> std::size_t {
const std::string needle = "\"" + std::string(name) + "\":";
const std::size_t at = line.find(needle);
if (at == std::string::npos) {
throw std::runtime_error("record lacks " + std::string(name));
}
return at + needle.size();
};
PublicState state;
const std::size_t board_at = field("board") + 1; // skip the opening quote
if (board_at + kCellCount > line.size()) {
throw std::runtime_error("truncated board");
}
for (int cell = 0; cell < kCellCount; ++cell) {
const char token = line[board_at + static_cast<std::size_t>(cell)];
if (token < '0' || token > '9') throw std::runtime_error("bad board");
state.board[static_cast<std::size_t>(cell)] =
static_cast<std::uint8_t>(token - '0');
}
state.next_disc =
static_cast<std::uint8_t>(std::stoi(line.substr(field("nextDisc"))));
state.moves_remaining = static_cast<std::uint8_t>(
std::stoi(line.substr(field("movesRemaining"))));
return stripToPublic(materialize(state));
}
void writeRelabel(std::ostream& output, const Relabel& value) {
output << "{\"scenarios\":" << kScenarios << ",\"horizon\":" << kHorizon
<< ",\"meanMoves\":" << numberText(value.mean_moves)
<< ",\"survived\":" << value.survived
<< ",\"revealsPerMove\":" << numberText(value.reveals_per_move)
<< ",\"clearsPerMove\":" << numberText(value.clears_per_move)
<< ",\"flowBand\":\"" << flowBandName(value.flow)
<< "\",\"scenarioMoves\":[";
for (int scenario = 0; scenario < kScenarios; ++scenario) {
if (scenario != 0) output << ',';
output << value.scenario_moves[scenario];
}
output << "],\"siblingMeanMoves\":[";
for (int column = 0; column < kBoardSize; ++column) {
if (column != 0) output << ',';
output << numberText(value.sibling_mean_moves[column]);
}
output << "],\"siblingSurvived\":[";
for (int column = 0; column < kBoardSize; ++column) {
if (column != 0) output << ',';
if (value.sibling_survived[column] < 0) {
output << "null";
} else {
output << value.sibling_survived[column];
}
}
output << "]}";
}
// ---- relabel driver ---------------------------------------------------------
struct RelabelOptions {
std::string input;
std::string output;
int threads = 8;
bool self_test = false;
};
std::vector<std::string> readLines(const std::string& path) {
std::ifstream input(path);
if (!input) throw std::runtime_error("cannot open " + path);
std::vector<std::string> lines;
std::string line;
while (std::getline(input, line)) {
if (!line.empty() && line.front() == '{') lines.push_back(line);
}
return lines;
}
template <typename Work>
void parallelFor(std::size_t count, int threads, Work&& work) {
std::atomic<std::size_t> next{0};
std::vector<std::future<void>> workers;
const int worker_count =
std::max(1, std::min<int>(threads, static_cast<int>(count)));
for (int worker = 0; worker < worker_count; ++worker) {
workers.push_back(std::async(std::launch::async, [&]() {
for (;;) {
const std::size_t index = next.fetch_add(1);
if (index >= count) return;
work(index);
}
}));
}
for (auto& worker : workers) worker.get();
}
int runRelabel(const RelabelOptions& options) {
const Deadline deadline;
const std::vector<std::string> lines = readLines(options.input);
std::vector<PublicState> states(lines.size());
for (std::size_t index = 0; index < lines.size(); ++index) {
states[index] = parsePublicState(lines[index]);
}
std::vector<Relabel> labels(states.size());
std::atomic<std::size_t> done{0};
parallelFor(states.size(), options.threads, [&](std::size_t index) {
labels[index] = relabel(states[index]);
const std::size_t finished = done.fetch_add(1) + 1;
if (finished % 200 == 0 || finished == states.size()) {
const std::lock_guard<std::mutex> lock(progress_mutex);
std::cerr << "d0-relabel " << finished << '/' << states.size()
<< " states " << std::fixed << std::setprecision(1)
<< deadline.elapsedSeconds() << "s\n";
}
});
enforceRssLimit();
std::ofstream output(options.output, std::ios::trunc);
if (!output) throw std::runtime_error("cannot write " + options.output);
output << "{\"format\":\"drop7-hpool-d0-pools-v1\",\"relabel\":{"
<< "\"scenarios\":" << kScenarios << ",\"horizon\":" << kHorizon
<< ",\"continuation\":\"fair D1 (fair-only-horizon rootDecision "
"depth 1, canonical frame each step)\",\"revealDomain\":\""
<< hexSeed(kRestartRevealDomain) << "\",\"visibleDomain\":\""
<< hexSeed(kRestartVisibleDomain) << "\",\"seedDomain\":\""
<< hex64(kRestartSeedDomain)
<< "\",\"threads\":" << options.threads
<< ",\"wallSeconds\":" << numberText(deadline.elapsedSeconds())
<< ",\"peakRssBytes\":" << peakRssBytes() << "},\"states\":[\n";
for (std::size_t index = 0; index < lines.size(); ++index) {
if (index != 0) output << ",\n";
std::string line = lines[index];
while (!line.empty() && line.back() != '}') line.pop_back();
line.pop_back();
output << line << ",\"publicHash\":\"" << hex64(publicHash(states[index]))
<< "\",\"relabel\":";
writeRelabel(output, labels[index]);
output << '}';
}
output << "\n]}\n";
output.close();
if (!output) throw std::runtime_error("could not finish " + options.output);
std::cerr << "d0-relabel wrote " << lines.size() << " states to "
<< options.output << " in " << std::fixed << std::setprecision(1)
<< deadline.elapsedSeconds() << "s\n";
return 0;
}
// Self-test on public records: mirror invariance (exact), metadata
// independence, domain separation, and sibling/root consistency.
int runRelabelSelfTest(const RelabelOptions& options) {
const std::vector<std::string> lines = readLines(options.input);
std::size_t tested = 0;
std::size_t mirror_failures = 0;
std::size_t metadata_failures = 0;
std::size_t sibling_failures = 0;
std::size_t stream_failures = 0;
std::vector<PublicState> states;
for (const std::string& line : lines) states.push_back(parsePublicState(line));
const std::size_t limit = std::min<std::size_t>(states.size(), 64);
std::vector<int> failures(limit * 4, 0);
parallelFor(limit, options.threads, [&](std::size_t index) {
const PublicState& state = states[index];
const Relabel base = relabel(state);
// (1) exact mirror invariance; siblings map to mirrored columns.
const Relabel mirrored = relabel(mirror(state));
bool mirror_ok = mirrored.mean_moves == base.mean_moves &&
mirrored.survived == base.survived &&
mirrored.scenario_moves == base.scenario_moves &&
mirrored.reveals_per_move == base.reveals_per_move;
for (int column = 0; column < kBoardSize; ++column) {
const double left = base.sibling_mean_moves[column];
const double right = mirrored.sibling_mean_moves[kBoardSize - 1 - column];
if (std::isnan(left) != std::isnan(right)) mirror_ok = false;
if (!std::isnan(left) && left != right) mirror_ok = false;
}
failures[index * 4 + 0] = !mirror_ok;
// (2) metadata independence: score / level / move counter are not in the
// public tuple and cannot change anything.
State origin = materialize(state);
origin.score = 9'876'543;
origin.level = 91;
origin.moves_played = 417;
const PublicState re_export = stripToPublic(origin);
failures[index * 4 + 1] =
!(re_export == state && restartSeed(re_export) == restartSeed(state) &&
relabel(re_export) == base);
// (3) the unforced restart equals the forced restart at D1's own column
// in scenario 0 (the forced path is the same path).
bool ignored = false;
const PublicState canonical = canonicalPublic(state, ignored);
const int d1 = chooseFairD1Canonical(canonical);
const int d1_source = ignored ? kBoardSize - 1 - d1 : d1;
failures[index * 4 + 2] =
!(runRestart(state, 0, d1_source) == runRestart(state, 0));
// (4) domain separation: for every scenario the visible-disc stream
// ("CRVS") differs from the first reveal stream ("CRRV") of the same
// scenario, and the scenarios are distinct as a set. Adjacent strata of
// the stratified visible stream legitimately coincide on a 25-step
// window now and then (32 strata over 7 disc values), so pairwise
// distinctness of the visible streams alone is not required; the
// concatenated (visible, reveal) streams of the 32 scenarios must all
// be distinct.
const std::uint32_t root_seed = restartSeed(state);
bool streams_ok = true;
std::vector<std::string> fingerprints;
for (int scenario = 0; scenario < kScenarios; ++scenario) {
std::string visible;
std::string reveal;
for (int step = 0; step < kHorizon; ++step) {
RestartRandom random{root_seed, scenario, step, 0};
visible.push_back(static_cast<char>(
'0' + restartVisibleDisc(root_seed, scenario, step)));
reveal.push_back(static_cast<char>('0' + random.nextDisc()));
}
if (visible == reveal) streams_ok = false;
fingerprints.push_back(visible + "|" + reveal);
}
std::sort(fingerprints.begin(), fingerprints.end());
if (std::adjacent_find(fingerprints.begin(), fingerprints.end()) !=
fingerprints.end()) {
streams_ok = false;
}
failures[index * 4 + 3] = !streams_ok;
});
for (std::size_t index = 0; index < limit; ++index) {
++tested;
mirror_failures += failures[index * 4 + 0];
metadata_failures += failures[index * 4 + 1];
sibling_failures += failures[index * 4 + 2];
stream_failures += failures[index * 4 + 3];
}
std::cout << "relabel-self-test states=" << tested
<< " mirror_failures=" << mirror_failures
<< " metadata_failures=" << metadata_failures
<< " sibling_consistency_failures=" << sibling_failures
<< " stream_separation_failures=" << stream_failures << '\n';
const bool ok = tested > 0 && mirror_failures == 0 &&
metadata_failures == 0 && sibling_failures == 0 &&
stream_failures == 0;
std::cout << (ok ? "PASS" : "FAIL") << " relabel-self-test\n";
return ok ? 0 : 1;
}
RelabelOptions parseRelabelOptions(int argc, char** argv, int begin) {
RelabelOptions result;
for (int index = begin; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--self-test") {
result.self_test = true;
continue;
}
if (index + 1 >= argc) throw std::invalid_argument("missing value");
const std::string value = argv[++index];
if (argument == "--input") {
result.input = value;
} else if (argument == "--output") {
result.output = value;
} else if (argument == "--threads") {
result.threads = std::stoi(value);
if (result.threads < 1 || result.threads > kMaximumThreads) {
throw std::invalid_argument("threads must be in [1,16]");
}
} else {
throw std::invalid_argument("unknown option " + argument);
}
}
if (result.input.empty()) throw std::invalid_argument("--input required");
if (!result.self_test && result.output.empty()) {
throw std::invalid_argument("--output required");
}
return result;
}
#ifdef D0_GENERATE
// ---- generation (privileged oracle + public fair D4) ------------------------
namespace oracle = drop7::oracle_topology;
struct GenerateOptions {
std::string output; // JSONL of public records
std::string summary; // JSON summary
int threads = 8;
bool probe = false; // probe seeds only (gate mode)
int oracle_games = kOracleGames;
int max_moves = kMaximumMoves;
int fair_max_moves = kMaximumMoves;
int fair_seeds = kFairSeeds;
int quota = kPerGameQuota;
double wall_limit = kWallLimitSeconds;
};
struct Visit {
PublicState state; // in the trajectory's own orientation
int move = 0; // moves_played at the state
int column = -1; // the column played from it (oracle or fair D4)
};
struct Trajectory {
std::uint32_t seed = 0;
int moves = 0;
std::int64_t score = 0;
bool censored = false;
double seconds = 0.0;
std::vector<Visit> visits; // every non-terminal state from move 50 on
};
bool seedAllowed(const GenerateOptions& options, std::uint32_t seed) {
if (options.probe) return seed >= kProbeSeedStart && seed < kProbeSeedEnd;
return (seed >= kOracleSeedStart && seed < kOracleSeedStart + kOracleGames) ||
(seed >= kFairSeedStart && seed < kFairSeedStart + kFairSeeds);
}
void requireSeed(const GenerateOptions& options, std::uint32_t seed) {
if (!seedAllowed(options, seed)) {
throw std::invalid_argument("seed " + hexSeed(seed) +
" is outside the allowed range");
}
}
Trajectory runOracleTrajectory(const GenerateOptions& options,
std::uint32_t seed) {
requireSeed(options, seed);
const auto started = Clock::now();
State state = initialHeadlessState(seed);
Trajectory result;
result.seed = seed;
while (!state.game_over && state.moves_played < options.max_moves) {
if (state.next_disc != headlessDisc(seed, state.moves_played)) {
throw std::runtime_error("oracle disc-stream guard failed");
}
const oracle::OraclePlan plan =
oracle::planOracleMove(state, seed, kOracleDepth, kOracleBeam);
if (!isLegal(state.board, plan.column)) {
throw std::runtime_error("oracle selected an illegal action");
}
if (state.moves_played >= kFirstSampleMove) {
result.visits.push_back(
{stripToPublic(state), state.moves_played, plan.column});
}
MoveResult move;
if (!playHeadlessMove(state, seed, plan.column, move)) {
throw std::runtime_error("oracle transition failed");
}
}
result.moves = state.moves_played;
result.score = state.score;
result.censored = !state.game_over;
result.seconds =
std::chrono::duration<double>(Clock::now() - started).count();
return result;
}
Trajectory runFairTrajectory(const GenerateOptions& options,
std::uint32_t seed) {
requireSeed(options, seed);
const auto started = Clock::now();
State state = initialHeadlessState(seed);
Trajectory result;
result.seed = seed;
while (!state.game_over && state.moves_played < options.fair_max_moves) {
const d4::SearchDecision decision = d4::chooseDepth4Action(state);
if (!decision.complete || !isLegal(state.board, decision.action)) {
throw std::runtime_error("fair depth four did not complete");
}
if (state.moves_played >= kFirstSampleMove) {
result.visits.push_back(
{stripToPublic(state), state.moves_played, decision.action});
}
MoveResult move;
if (!playHeadlessMove(state, seed, decision.action, move)) {
throw std::runtime_error("fair transition failed");
}
}
result.moves = state.moves_played;
result.score = state.score;
result.censored = !state.game_over;
result.seconds =
std::chrono::duration<double>(Clock::now() - started).count();
return result;
}
// Fair D4's column at a public root. Public information only.
int fairD4ColumnAt(const PublicState& state) {
const d4::SearchDecision decision =
d4::chooseDepth4Action(materialize(state));
if (!decision.complete || !isLegal(state.board, decision.action)) {
throw std::runtime_error("fair depth four did not complete at O root");
}
return decision.action;
}
struct Record {
char pool = 'O';
int game = 0;
std::uint32_t seed = 0;
int move = 0;
PublicState state;
int column = -1; // played column (oracle for O, fair D4 for F)
int d4_column = -1; // fair D4 at the root (O only; equals column for F)
int remaining = 0; // realised remaining moves, uncapped
bool remaining_censored = false; // origin game hit the move cap first
Bucket bucket;
int match = -1; // index of the partner record
int half = 0; // origin-game half (O: game < games/2)
};
constexpr std::uint32_t kSampleDomain = 0x4430'5350u; // "D0SP"
// One visit per stratum: the eligible visits are cut into `quota` equal
// strata and one index is drawn inside each with a seed-keyed hash. A
// centred evenly-spaced rule aliases with the five-move rise cadence (a
// stride that is a multiple of five samples a single rise phase), so the
// within-stratum offset is jittered instead.
int stratifiedIndex(std::uint32_t seed, int slot, int quota, int count) {
const int begin = static_cast<int>(static_cast<long long>(slot) * count / quota);
const int end =
static_cast<int>(static_cast<long long>(slot + 1) * count / quota);
const int length = std::max(1, end - begin);
const std::uint32_t draw = mix32(
seed ^ (static_cast<std::uint32_t>(slot + 1) * 0x9e37'79b9u) ^
kSampleDomain);
return std::min(count - 1, begin + static_cast<int>(draw % static_cast<std::uint32_t>(length)));
}
void writeRecord(std::ostream& output, const Record& record, int index) {
output << "{\"id\":" << index << ",\"pool\":\"" << record.pool
<< "\",\"game\":" << record.game << ",\"seed\":\""
<< hexSeed(record.seed) << "\",\"move\":" << record.move
<< ",\"state\":";
writePublicState(output, record.state);
output << ",\"column\":" << record.column
<< ",\"d4Column\":" << record.d4_column
<< ",\"remainingRealised\":" << record.remaining
<< ",\"remainingCapped\":" << std::min(record.remaining, kHorizon)
<< ",\"remainingCensored\":"
<< (record.remaining_censored ? "true" : "false")
<< ",\"bucket\":{\"risePhase\":" << record.bucket.rise_phase
<< ",\"occupancyBin\":" << record.bucket.occupancy_bin
<< ",\"heightBin\":" << record.bucket.height_bin
<< "},\"match\":" << record.match << ",\"half\":" << record.half
<< "}\n";
}
int runGenerate(const GenerateOptions& options) {
Deadline deadline;
deadline.limit = options.wall_limit;
const std::uint32_t oracle_start =
options.probe ? kProbeSeedStart : kOracleSeedStart;
const std::uint32_t fair_start =
options.probe ? kProbeSeedStart + 0x100u : kFairSeedStart;
bool partial = false;
std::string stop_reason = "completed";
// --- pool O -------------------------------------------------------------
std::vector<Trajectory> oracle_games(
static_cast<std::size_t>(options.oracle_games));
parallelFor(oracle_games.size(), options.threads, [&](std::size_t index) {
const std::uint32_t seed = oracle_start + static_cast<std::uint32_t>(index);
oracle_games[index] = runOracleTrajectory(options, seed);
const std::lock_guard<std::mutex> lock(progress_mutex);
std::cerr << "d0-generate oracle " << index + 1 << '/'
<< oracle_games.size() << " seed " << hexSeed(seed) << " moves "
<< oracle_games[index].moves << " score "
<< oracle_games[index].score << " visits "
<< oracle_games[index].visits.size() << '\n';
});
enforceRssLimit();
std::vector<Record> records;
std::uint64_t oracle_visits_total = 0;
for (std::size_t game = 0; game < oracle_games.size(); ++game) {
const Trajectory& trajectory = oracle_games[game];
const int count = static_cast<int>(trajectory.visits.size());
oracle_visits_total += static_cast<std::uint64_t>(count);
const int take = std::min(count, options.quota);
for (int slot = 0; slot < take; ++slot) {
const int pick =
count <= options.quota
? slot
: stratifiedIndex(trajectory.seed, slot, options.quota, count);
const Visit& visit = trajectory.visits[static_cast<std::size_t>(pick)];
Record record;
record.pool = 'O';
record.game = static_cast<int>(game);
record.seed = trajectory.seed;
record.move = visit.move;
record.state = visit.state;
record.column = visit.column;
record.remaining = trajectory.moves - visit.move;
record.remaining_censored =
trajectory.censored && record.remaining < kHorizon;
record.bucket = bucketOf(visit.state);
record.half = game < oracle_games.size() / 2 ? 0 : 1;
records.push_back(record);
}
}
const std::size_t o_count = records.size();
std::cerr << "d0-generate pool O: " << o_count << " states from "
<< oracle_visits_total << " eligible visits\n";
// fair D4's column at every O root (public computation).
parallelFor(o_count, options.threads, [&](std::size_t index) {
records[index].d4_column = fairD4ColumnAt(records[index].state);
if ((index + 1) % 100 == 0) {
const std::lock_guard<std::mutex> lock(progress_mutex);
std::cerr << "d0-generate fair-D4 at O roots " << index + 1 << '/'
<< o_count << " " << std::fixed << std::setprecision(1)
<< deadline.elapsedSeconds() << "s\n";
}
});
enforceRssLimit();
// --- pool F, generated lazily in fixed batches of 16 seeds ---------------
std::map<Bucket, std::vector<std::size_t>> demand;
for (std::size_t index = 0; index < o_count; ++index) {
demand[records[index].bucket].push_back(index);
}
std::size_t open_demand = o_count;
int fair_games_played = 0;
std::uint64_t fair_visits_total = 0;
std::vector<Trajectory> fair_games;
for (int batch_start = 0;
batch_start < options.fair_seeds && open_demand > 0;
batch_start += kFairBatch) {
if (deadline.expired()) {
partial = true;
stop_reason = "wall limit reached before pool F was served";
break;
}
const int batch_count =
std::min(kFairBatch, options.fair_seeds - batch_start);
std::vector<Trajectory> batch(static_cast<std::size_t>(batch_count));
parallelFor(batch.size(), options.threads, [&](std::size_t index) {
const std::uint32_t seed =
fair_start + static_cast<std::uint32_t>(batch_start) +
static_cast<std::uint32_t>(index);
batch[index] = runFairTrajectory(options, seed);
const std::lock_guard<std::mutex> lock(progress_mutex);
std::cerr << "d0-generate fair " << batch_start + static_cast<int>(index) + 1
<< '/' << options.fair_seeds << " seed " << hexSeed(seed)
<< " moves " << batch[index].moves << " score "
<< batch[index].score << " visits " << batch[index].visits.size()
<< " " << std::fixed << std::setprecision(1)
<< deadline.elapsedSeconds() << "s\n";
});
enforceRssLimit();
for (std::size_t local = 0; local < batch.size(); ++local) {
const Trajectory& trajectory = batch[local];
const int game = batch_start + static_cast<int>(local);
++fair_games_played;
fair_visits_total += trajectory.visits.size();
std::map<Bucket, int> used;
for (const Visit& visit : trajectory.visits) {
if (open_demand == 0) break;
const Bucket bucket = bucketOf(visit.state);
auto found = demand.find(bucket);
if (found == demand.end() || found->second.empty()) continue;
if (used[bucket] >= kPerGameBucketCap) continue;
++used[bucket];
const std::size_t partner = found->second.front();
found->second.erase(found->second.begin());
--open_demand;
Record record;
record.pool = 'F';
record.game = game;
record.seed = trajectory.seed;
record.move = visit.move;
record.state = visit.state;
record.column = visit.column;
record.d4_column = visit.column;
record.remaining = trajectory.moves - visit.move;
record.remaining_censored =
trajectory.censored && record.remaining < kHorizon;
record.bucket = bucket;
record.match = static_cast<int>(partner);
record.half = records[partner].half;
records[partner].match = static_cast<int>(records.size());
records.push_back(record);
}
fair_games.push_back(trajectory);
}
std::cerr << "d0-generate pool F: " << records.size() - o_count
<< " matched after " << fair_games_played << " fair games; "
<< open_demand << " O states still unmatched\n";
}
const std::size_t matched = records.size() - o_count;
const std::size_t unmatched = open_demand;
// --- write ----------------------------------------------------------------
{
std::ofstream output(options.output, std::ios::trunc);
if (!output) throw std::runtime_error("cannot write " + options.output);
for (std::size_t index = 0; index < records.size(); ++index) {
writeRecord(output, records[index], static_cast<int>(index));
}
output.close();
if (!output) throw std::runtime_error("could not finish records");
}
{
std::ofstream output(options.summary, std::ios::trunc);
if (!output) throw std::runtime_error("cannot write " + options.summary);
output << "{\"format\":\"drop7-hpool-d0-generate-v1\",\"probe\":"
<< (options.probe ? "true" : "false")
<< ",\"partial\":" << (partial ? "true" : "false")
<< ",\"stopReason\":\"" << stop_reason << "\""
<< ",\"oracle\":{\"seedStart\":\"" << hexSeed(oracle_start)
<< "\",\"games\":" << options.oracle_games
<< ",\"depth\":" << kOracleDepth << ",\"beam\":" << kOracleBeam
<< ",\"maxMoves\":" << options.max_moves
<< ",\"firstSampleMove\":" << kFirstSampleMove
<< ",\"quotaPerGame\":" << options.quota
<< ",\"samplingRule\":\"per game, take min(quota, eligible) "
"visits; when eligible > quota pick index "
"begin + mix32(seed ^ (slot+1)*0x9e3779b9 ^ 0x44305350) % (end-begin) "
"with begin=floor(slot*eligible/quota), end=floor((slot+1)*"
"eligible/quota) for slot in [0,quota) (one visit per equal "
"stratum, seed-keyed offset to avoid aliasing with the five-"
"move rise cadence); eligible = non-terminal states with "
"moves_played >= 50 in move order\""
<< ",\"eligibleVisits\":" << oracle_visits_total
<< ",\"states\":" << o_count << ",\"games\":[";
for (std::size_t game = 0; game < oracle_games.size(); ++game) {
const Trajectory& trajectory = oracle_games[game];
if (game != 0) output << ',';
output << "{\"seed\":\"" << hexSeed(trajectory.seed) << "\",\"moves\":"
<< trajectory.moves << ",\"score\":" << trajectory.score
<< ",\"censored\":" << (trajectory.censored ? "true" : "false")
<< ",\"visits\":" << trajectory.visits.size()
<< ",\"seconds\":" << numberText(trajectory.seconds) << '}';
}
output << "]},\"fair\":{\"seedStart\":\"" << hexSeed(fair_start)
<< "\",\"seedsAvailable\":" << options.fair_seeds
<< ",\"gamesPlayed\":" << fair_games_played
<< ",\"batch\":" << kFairBatch
<< ",\"perGameBucketCap\":" << kPerGameBucketCap
<< ",\"maxMoves\":" << options.fair_max_moves
<< ",\"search\":\"fair-only-depth4 reference (depth 4, five "
"strata, 3,200,000 work cap)\""
<< ",\"eligibleVisits\":" << fair_visits_total
<< ",\"matchingRule\":\"buckets (risePhase, occupancy/4, "
"maxHeight/2) as oracle-topology-audit.cpp; F visits consumed "
"in (seed, move) order, at most 2 per bucket per F game, each "
"assigned to the earliest unmatched O state of its bucket; "
"generation stops when every O state is matched or the seeds "
"are exhausted\""
<< ",\"games\":[";
for (std::size_t game = 0; game < fair_games.size(); ++game) {
const Trajectory& trajectory = fair_games[game];
if (game != 0) output << ',';
output << "{\"seed\":\"" << hexSeed(trajectory.seed) << "\",\"moves\":"
<< trajectory.moves << ",\"score\":" << trajectory.score
<< ",\"censored\":" << (trajectory.censored ? "true" : "false")
<< ",\"visits\":" << trajectory.visits.size()
<< ",\"seconds\":" << numberText(trajectory.seconds) << '}';
}
output << "]},\"matching\":{\"oStates\":" << o_count
<< ",\"matched\":" << matched << ",\"unmatched\":" << unmatched
<< ",\"buckets\":" << demand.size() << ",\"unservedBuckets\":[";
bool first = true;
for (const auto& [bucket, waiting] : demand) {
if (waiting.empty()) continue;
if (!first) output << ',';
first = false;
output << "{\"risePhase\":" << bucket.rise_phase << ",\"occupancyBin\":"
<< bucket.occupancy_bin << ",\"heightBin\":" << bucket.height_bin
<< ",\"unmatched\":" << waiting.size() << '}';
}
output << "]},\"threads\":" << options.threads
<< ",\"wallSeconds\":" << numberText(deadline.elapsedSeconds())
<< ",\"peakRssBytes\":" << peakRssBytes() << "}\n";
output.close();
if (!output) throw std::runtime_error("could not finish summary");
}
std::cerr << "d0-generate done: O=" << o_count << " F=" << matched
<< " unmatched=" << unmatched << " fair games="
<< fair_games_played << " wall " << std::fixed
<< std::setprecision(1) << deadline.elapsedSeconds() << "s"
<< (partial ? " PARTIAL" : "") << '\n';
return partial ? 3 : 0;
}
GenerateOptions parseGenerateOptions(int argc, char** argv, int begin) {
GenerateOptions result;
for (int index = begin; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--probe") {
result.probe = true;
continue;
}
if (index + 1 >= argc) throw std::invalid_argument("missing value");
const std::string value = argv[++index];
if (argument == "--output") {
result.output = value;
} else if (argument == "--summary") {
result.summary = value;
} else if (argument == "--threads") {
result.threads = std::stoi(value);
if (result.threads < 1 || result.threads > kMaximumThreads) {
throw std::invalid_argument("threads must be in [1,16]");
}
} else if (argument == "--oracle-games") {
result.oracle_games = std::stoi(value);
} else if (argument == "--max-moves") {
result.max_moves = std::stoi(value);
} else if (argument == "--fair-max-moves") {
result.fair_max_moves = std::stoi(value);
} else if (argument == "--fair-seeds") {
result.fair_seeds = std::stoi(value);
} else if (argument == "--quota") {
result.quota = std::stoi(value);
} else if (argument == "--wall-limit") {
result.wall_limit = std::stod(value);
} else {
throw std::invalid_argument("unknown option " + argument);
}
}
if (result.output.empty() || result.summary.empty()) {
throw std::invalid_argument("--output and --summary are required");
}
if (!result.probe) {
// The frozen configuration may not be altered on the leased seeds.
if (result.oracle_games != kOracleGames || result.max_moves != kMaximumMoves ||
result.fair_max_moves != kMaximumMoves || result.fair_seeds != kFairSeeds ||
result.quota != kPerGameQuota || result.wall_limit > kWallLimitSeconds) {
throw std::invalid_argument(
"non-probe runs use the frozen configuration only");
}
} else {
if (result.oracle_games < 1 || result.oracle_games > 0x100 ||
result.fair_seeds < 1 || result.fair_seeds > 0x400 ||
result.quota < 1 || result.max_moves < kFirstSampleMove + 1 ||
result.fair_max_moves < kFirstSampleMove + 1) {
throw std::invalid_argument("probe configuration out of range");
}
}
return result;
}
#endif // D0_GENERATE
} // namespace drop7::hpool_d0
int main(int argc, char** argv) {
try {
#ifdef D0_GENERATE
if (argc >= 2 && std::string_view(argv[1]) == "--generate") {
return drop7::hpool_d0::runGenerate(
drop7::hpool_d0::parseGenerateOptions(argc, argv, 2));
}
std::cerr << "usage: d0-generate --generate --output RECORDS.jsonl "
"--summary SUMMARY.json [--threads N] [--probe "
"--oracle-games N --max-moves N --fair-max-moves N "
"--fair-seeds N --quota N]\n";
return 2;
#else
if (argc >= 2 && std::string_view(argv[1]) == "--relabel") {
const auto options = drop7::hpool_d0::parseRelabelOptions(argc, argv, 2);
if (options.self_test) {
return drop7::hpool_d0::runRelabelSelfTest(options);
}
return drop7::hpool_d0::runRelabel(options);
}
std::cerr << "usage: d0-relabel --relabel --input RECORDS.jsonl "
"(--output POOLS.json | --self-test) [--threads N]\n";
return 2;
#endif
} catch (const std::exception& error) {
std::cerr << "hpool-d0: " << error.what() << '\n';
return 1;
}
}