#define main drop7_fair_only_horizon_embedded_main
#include "../reference/fair-only-horizon.cpp"
#undef main
#include <atomic>
#include <bit>
#include <optional>
#include <sstream>
// A deliberately small, public-information rollout pilot. Every legal root
// action is evaluated on the same seven deterministic, stratified scenario
// tapes. The first action is fixed by the root candidate; every later action
// is selected by the fair depth-one policy without access to the tape. Reveal
// draws and future visible discs occupy separate event-indexed domains.
namespace drop7::fair_d1_rollout_improvement {
namespace fair = fair_only_horizon;
using Clock = std::chrono::steady_clock;
constexpr std::uint32_t kFittingStart = 0x3df0'0000u;
constexpr int kFittingGames = 12;
constexpr std::uint32_t kHeldoutStart = 0x3df1'0000u;
constexpr int kHeldoutGames = 16;
constexpr int kMaximumMoves = 1'000;
constexpr int kScenarios = 7;
constexpr int kDefaultThreads = 4;
constexpr double kTerminalUtility = -1'000'000.0;
constexpr double kMinimumFittingMean = 250'000.0;
constexpr double kMinimumClearThroughputRatio = 1.05;
constexpr int kMinimumLooScoreWins = 9;
constexpr std::uint32_t kTapeSeedDomain = 0x4652'5450u; // "FRTP"
constexpr std::uint32_t kRevealTapeDomain = 0x4652'564cu; // "FRVL"
constexpr std::uint32_t kVisibleTapeDomain = 0x4656'4953u; // "FVIS"
constexpr int kEventsPerStep = 64;
struct Config {
int horizon;
double tail_scale;
};
// Preregistered before running any fitting game.
constexpr std::array<Config, 6> kConfigs{{
{8, 0.0}, {8, 0.25}, {16, 0.0},
{16, 0.25}, {24, 0.0}, {24, 0.25},
}};
constexpr std::array<int, kBoardSize> kColumnOrder{{3, 2, 4, 1, 5, 0, 6}};
constexpr std::uint64_t kMaximumTransitionsPerDecision =
static_cast<std::uint64_t>(kBoardSize) * kScenarios *
kConfigs.back().horizon * (kBoardSize + 1);
static_assert(kLevelBonus == 7'000);
static_assert(kScenarios == kBoardSize);
static_assert(kEventsPerStep > kCellCount);
static_assert(kMaximumTransitionsPerDecision == 9'408);
static_assert((kFittingStart >> 24u) != 0x3eu &&
(kFittingStart >> 24u) != 0x7du &&
(kFittingStart >> 24u) != 0xd7u);
static_assert((kHeldoutStart >> 24u) != 0x3eu &&
(kHeldoutStart >> 24u) != 0x7du &&
(kHeldoutStart >> 24u) != 0xd7u);
static_assert(kFittingStart + kFittingGames <= kHeldoutStart);
std::mutex progress_mutex;
State publicState(const State& source) {
State result;
result.board = source.board;
result.next_disc = source.next_disc;
result.moves_remaining = source.moves_remaining;
result.game_over = source.game_over;
result.score = 0;
result.level = 1;
result.moves_played = 0;
return result;
}
bool samePublicState(const State& left_source, const State& right_source) {
const State left = publicState(left_source);
const State right = publicState(right_source);
return left.board == right.board && left.next_disc == right.next_disc &&
left.moves_remaining == right.moves_remaining &&
left.game_over == right.game_over;
}
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 State& source) {
bool ignored = false;
const State state =
cfpi::detail::canonicalState(publicState(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 + 1);
hash *= 0x0000'0100'0000'01b3ull;
hash ^= static_cast<std::uint64_t>(state.game_over);
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));
}
struct Work {
std::uint64_t transitions = 0;
std::uint64_t fair_d1_calls = 0;
};
int fairDepthOneAction(const State& source, Work* work = nullptr) {
if (source.game_over) return -1;
bool mirrored = false;
const State state = cfpi::detail::canonicalState(publicState(source), mirrored);
const std::uint32_t chance_seed = cfpi::detail::scenarioSeedForState(
state, fair::kPolicySeed, 1);
int selected = -1;
double best = -std::numeric_limits<double>::infinity();
if (work != nullptr) ++work->fair_d1_calls;
for (const int action : kColumnOrder) {
if (!isLegal(state.board, action)) continue;
cfpi::detail::StratifiedRandom random{chance_seed, 0, 1, 0};
MoveResult move;
if (!cfpi::detail::playMoveSampled(state, action, random, move)) continue;
if (work != nullptr) ++work->transitions;
double value = static_cast<double>(move.score_delta);
if (move.state.game_over) {
value += fair::kTerminalUtility;
} else {
move.state = publicState(move.state);
move.state.next_disc =
cfpi::detail::sampledNextDisc(chance_seed, 0, 1);
value += fair::fairLeaf(move.state);
}
if (value > best) {
best = value;
selected = action;
}
}
if (selected < 0) selected = centerFirstMove(state.board);
return mirrored && selected >= 0 ? kBoardSize - 1 - selected : selected;
}
struct TapeDomains {
std::uint32_t reveal = kRevealTapeDomain;
std::uint32_t visible = kVisibleTapeDomain;
};
struct RevealTape {
std::uint32_t root_seed = 0;
int scenario = 0;
int step = 0;
std::uint32_t domain = kRevealTapeDomain;
int event = 0;
std::uint8_t nextDisc() {
const int event_index = step * kEventsPerStep + event++;
const double unit = cfpi::detail::stratifiedUnit(
root_seed, scenario, kScenarios, domain, event_index);
return static_cast<std::uint8_t>(
std::floor(unit * static_cast<double>(kBoardSize)) + 1.0);
}
};
std::uint8_t visibleDisc(std::uint32_t root_seed, int scenario, int step,
std::uint32_t domain = kVisibleTapeDomain) {
const double unit = cfpi::detail::stratifiedUnit(
root_seed, scenario, kScenarios, domain, step);
return static_cast<std::uint8_t>(
std::floor(unit * static_cast<double>(kBoardSize)) + 1.0);
}
bool playSyntheticMove(const State& source, int action,
std::uint32_t root_seed, int scenario, int step,
MoveResult& result, Work* work = nullptr,
TapeDomains domains = {}) {
const State state = publicState(source);
if (state.game_over) return false;
Board board = state.board;
if (!placeDisc(board, action, state.next_disc)) return false;
RevealTape reveals{root_seed, scenario, step, domains.reveal, 0};
result = MoveResult{};
std::int64_t first_score = 0;
cfpi::detail::resolveCascadeSampled(board, reveals, 1, first_score,
result.waves);
result.score_delta = first_score;
result.cleared_board = isBoardEmpty(board);
if (result.cleared_board) result.score_delta += kClearBonus;
int moves_remaining = state.moves_remaining - 1;
bool game_over = false;
if (moves_remaining == 0) {
Board raised{};
if (!raiseCoveredRow(board, raised)) {
game_over = true;
} else {
result.level_advanced = true;
moves_remaining = kMovesPerLevel;
result.score_delta += kLevelBonus;
board = raised;
std::int64_t level_score = 0;
const int next_depth =
result.waves.empty() ? 1 : result.waves.back().depth + 1;
cfpi::detail::resolveCascadeSampled(board, reveals, next_depth,
level_score, result.waves);
result.score_delta += level_score;
if (isBoardEmpty(board)) {
result.score_delta += kClearBonus;
result.cleared_board = true;
}
}
}
int legal_count = 0;
legalColumns(board, legal_count);
if (!game_over && legal_count == 0) game_over = true;
result.state.board = board;
result.state.next_disc =
game_over ? state.next_disc
: visibleDisc(root_seed, scenario, step, domains.visible);
result.state.score = 0;
result.state.level = 1;
result.state.moves_remaining = moves_remaining;
result.state.moves_played = 0;
result.state.game_over = game_over;
if (work != nullptr) ++work->transitions;
return true;
}
struct Decision {
int action = -1;
std::array<double, kBoardSize> values{};
std::array<std::array<double, kScenarios>, kBoardSize> scenario_returns{};
Work work{};
};
Decision chooseRolloutAction(const State& source, const Config& config) {
Decision result;
result.values.fill(-std::numeric_limits<double>::infinity());
for (auto& returns : result.scenario_returns) {
returns.fill(-std::numeric_limits<double>::infinity());
}
if (source.game_over) return result;
bool mirrored = false;
const State root = cfpi::detail::canonicalState(publicState(source), mirrored);
const std::uint32_t tape_seed =
seed32(publicHash(root) ^ static_cast<std::uint64_t>(kTapeSeedDomain));
int selected = -1;
double best = -std::numeric_limits<double>::infinity();
for (const int action : kColumnOrder) {
if (!isLegal(root.board, action)) continue;
double sum = 0.0;
for (int scenario = 0; scenario < kScenarios; ++scenario) {
State state = root;
double value = 0.0;
for (int step = 0; step < config.horizon && !state.game_over; ++step) {
const int next_action =
step == 0 ? action : fairDepthOneAction(state, &result.work);
if (!isLegal(state.board, next_action)) {
value += kTerminalUtility;
state.game_over = true;
break;
}
MoveResult move;
if (!playSyntheticMove(state, next_action, tape_seed, scenario, step,
move, &result.work)) {
value += kTerminalUtility;
state.game_over = true;
break;
}
value += static_cast<double>(move.score_delta);
state = publicState(move.state);
if (state.game_over) value += kTerminalUtility;
}
if (!state.game_over) value += config.tail_scale * fair::fairLeaf(state);
result.scenario_returns[action][scenario] = value;
sum += value;
}
const double value = sum / static_cast<double>(kScenarios);
result.values[action] = value;
if (value > best) {
best = value;
selected = action;
}
}
if (selected < 0) selected = centerFirstMove(root.board);
if (!mirrored) {
result.action = selected;
return result;
}
std::array<double, kBoardSize> source_values{};
std::array<std::array<double, kScenarios>, kBoardSize> source_returns{};
for (int column = 0; column < kBoardSize; ++column) {
source_values[kBoardSize - 1 - column] = result.values[column];
source_returns[kBoardSize - 1 - column] = result.scenario_returns[column];
}
result.values = source_values;
result.scenario_returns = source_returns;
result.action = selected < 0 ? selected : kBoardSize - 1 - selected;
return result;
}
struct GameResult {
std::uint32_t seed = 0;
std::int64_t score = 0;
int moves = 0;
std::int64_t numbered_cleared = 0;
std::int64_t covers_revealed = 0;
int clear_boards = 0;
int maximum_chain = 0;
bool censored = false;
double decision_seconds = 0.0;
std::uint64_t transitions = 0;
std::uint64_t fair_d1_calls = 0;
};
enum class Policy { kFairD1, kRollout };
GameResult runGame(std::uint32_t seed, Policy policy,
const Config* config = nullptr) {
State state = initialHeadlessState(seed);
GameResult result;
result.seed = seed;
while (!state.game_over && state.moves_played < kMaximumMoves) {
const auto started = Clock::now();
int action = -1;
Work work;
if (policy == Policy::kFairD1) {
action = fairDepthOneAction(publicState(state), &work);
} else {
if (config == nullptr) throw std::invalid_argument("missing rollout config");
const Decision decision = chooseRolloutAction(publicState(state), *config);
action = decision.action;
work = decision.work;
if (work.transitions > kMaximumTransitionsPerDecision) {
throw std::runtime_error("rollout exceeded preregistered work bound");
}
}
result.decision_seconds +=
std::chrono::duration<double>(Clock::now() - started).count();
result.transitions += work.transitions;
result.fair_d1_calls += work.fair_d1_calls;
if (!isLegal(state.board, action)) {
throw std::runtime_error("policy returned illegal action");
}
MoveResult move;
if (!playHeadlessMove(state, seed, action, move)) {
throw std::runtime_error("headless transition failed");
}
for (const Wave& wave : move.waves) {
result.numbered_cleared += wave.cleared;
result.covers_revealed += wave.revealed;
result.maximum_chain = std::max(result.maximum_chain, wave.depth);
}
if (move.cleared_board) ++result.clear_boards;
}
result.score = state.score;
result.moves = state.moves_played;
result.censored = !state.game_over;
return result;
}
std::vector<GameResult> runCohort(std::uint32_t start, int games,
Policy policy, const Config* config,
int threads, std::string_view label) {
std::vector<GameResult> result(static_cast<std::size_t>(games));
std::atomic<int> next{0};
std::atomic<int> completed{0};
const int worker_count = std::max(1, std::min(threads, games));
std::vector<std::future<void>> workers;
workers.reserve(static_cast<std::size_t>(worker_count));
for (int worker = 0; worker < worker_count; ++worker) {
workers.push_back(std::async(std::launch::async, [&, worker]() {
static_cast<void>(worker);
for (;;) {
const int game = next.fetch_add(1);
if (game >= games) break;
const std::uint32_t seed = start + static_cast<std::uint32_t>(game);
result[static_cast<std::size_t>(game)] = runGame(seed, policy, config);
const int done = completed.fetch_add(1) + 1;
const std::lock_guard<std::mutex> lock(progress_mutex);
std::cerr << "fair-d1-rollout " << label << ' ' << done << '/'
<< games << " seed 0x" << std::hex << seed << std::dec
<< " score " << result[static_cast<std::size_t>(game)].score
<< " moves " << result[static_cast<std::size_t>(game)].moves
<< '\n';
}
}));
}
for (auto& worker : workers) worker.get();
return result;
}
double quantile(std::vector<double> values, double probability) {
if (values.empty()) return 0.0;
std::sort(values.begin(), values.end());
const double position = probability * static_cast<double>(values.size() - 1);
const std::size_t lower = static_cast<std::size_t>(std::floor(position));
const std::size_t upper = static_cast<std::size_t>(std::ceil(position));
const double fraction = position - static_cast<double>(lower);
return values[lower] * (1.0 - fraction) + values[upper] * fraction;
}
struct Summary {
int games = 0;
int censored = 0;
double mean_score = 0.0;
double median_score = 0.0;
double lower_quartile_score = 0.0;
double minimum_score = 0.0;
double maximum_score = 0.0;
double standard_error = 0.0;
double mean_moves = 0.0;
double clears_per_move = 0.0;
double reveals_per_move = 0.0;
double mean_decision_ms = 0.0;
double mean_transitions_per_decision = 0.0;
std::int64_t total_clears = 0;
std::int64_t total_reveals = 0;
std::int64_t total_moves = 0;
};
Summary summarize(const std::vector<GameResult>& games,
std::optional<int> omitted = std::nullopt) {
Summary result;
std::vector<double> scores;
double score_sum = 0.0;
double score_square_sum = 0.0;
double decision_seconds = 0.0;
std::uint64_t transitions = 0;
for (std::size_t index = 0; index < games.size(); ++index) {
if (omitted.has_value() && static_cast<int>(index) == *omitted) continue;
const GameResult& game = games[index];
++result.games;
result.censored += game.censored ? 1 : 0;
const double score = static_cast<double>(game.score);
scores.push_back(score);
score_sum += score;
score_square_sum += score * score;
result.total_moves += game.moves;
result.total_clears += game.numbered_cleared;
result.total_reveals += game.covers_revealed;
decision_seconds += game.decision_seconds;
transitions += game.transitions;
}
if (result.games == 0) return result;
result.mean_score = score_sum / result.games;
result.median_score = quantile(scores, 0.5);
result.lower_quartile_score = quantile(scores, 0.25);
result.minimum_score = *std::min_element(scores.begin(), scores.end());
result.maximum_score = *std::max_element(scores.begin(), scores.end());
if (result.games > 1) {
const double variance = std::max(
0.0, (score_square_sum - score_sum * score_sum / result.games) /
static_cast<double>(result.games - 1));
result.standard_error = std::sqrt(variance / result.games);
}
result.mean_moves = static_cast<double>(result.total_moves) / result.games;
if (result.total_moves > 0) {
result.clears_per_move =
static_cast<double>(result.total_clears) / result.total_moves;
result.reveals_per_move =
static_cast<double>(result.total_reveals) / result.total_moves;
result.mean_decision_ms =
1'000.0 * decision_seconds / result.total_moves;
result.mean_transitions_per_decision =
static_cast<double>(transitions) / result.total_moves;
}
return result;
}
struct LeaveOneOut {
std::array<int, kConfigs.size()> winner_counts{};
int global_score_wins = 0;
int global_throughput_wins = 0;
bool gains_survive = false;
double global_mean_min = std::numeric_limits<double>::infinity();
double global_mean_max = -std::numeric_limits<double>::infinity();
};
int selectConfig(const std::array<Summary, kConfigs.size()>& summaries) {
int selected = 0;
for (std::size_t index = 1; index < summaries.size(); ++index) {
if (summaries[index].mean_score > summaries[selected].mean_score ||
(summaries[index].mean_score == summaries[selected].mean_score &&
summaries[index].clears_per_move >
summaries[selected].clears_per_move)) {
selected = static_cast<int>(index);
}
}
return selected;
}
LeaveOneOut leaveOneOut(
const std::vector<GameResult>& baseline,
const std::array<std::vector<GameResult>, kConfigs.size()>& candidates,
int global_champion) {
LeaveOneOut result;
for (int omitted = 0; omitted < kFittingGames; ++omitted) {
std::array<Summary, kConfigs.size()> summaries{};
for (std::size_t config = 0; config < kConfigs.size(); ++config) {
summaries[config] = summarize(candidates[config], omitted);
}
const int winner = selectConfig(summaries);
++result.winner_counts[static_cast<std::size_t>(winner)];
const Summary baseline_summary = summarize(baseline, omitted);
const Summary champion_summary =
summaries[static_cast<std::size_t>(global_champion)];
result.global_mean_min =
std::min(result.global_mean_min, champion_summary.mean_score);
result.global_mean_max =
std::max(result.global_mean_max, champion_summary.mean_score);
if (champion_summary.mean_score > baseline_summary.mean_score) {
++result.global_score_wins;
}
if (champion_summary.clears_per_move >=
baseline_summary.clears_per_move * kMinimumClearThroughputRatio) {
++result.global_throughput_wins;
}
}
result.gains_survive =
result.global_score_wins >= kMinimumLooScoreWins &&
result.global_throughput_wins >= kMinimumLooScoreWins;
return result;
}
std::uint64_t peakRssBytes() {
rusage usage{};
if (getrusage(RUSAGE_SELF, &usage) != 0) return 0;
#if defined(__APPLE__)
return static_cast<std::uint64_t>(usage.ru_maxrss);
#else
return static_cast<std::uint64_t>(usage.ru_maxrss) * 1024u;
#endif
}
void expect(bool condition, std::string_view message) {
if (!condition) throw std::runtime_error(std::string(message));
}
State asymmetricFixture() {
State state;
state.board.fill(kEmpty);
state.board[indexOf(6, 0)] = kSolid;
state.board[indexOf(6, 1)] = 4;
state.board[indexOf(6, 2)] = 2;
state.board[indexOf(5, 2)] = kCracked;
state.board[indexOf(6, 4)] = 6;
state.next_disc = 3;
state.moves_remaining = 3;
return state;
}
void runSelfTests() {
const Config config{8, 0.25};
const State fixture = asymmetricFixture();
const Decision first = chooseRolloutAction(fixture, config);
const Decision second = chooseRolloutAction(fixture, config);
expect(first.action == second.action && first.values == second.values &&
first.scenario_returns == second.scenario_returns &&
first.work.transitions == second.work.transitions,
"rollout must be deterministic");
expect(first.work.transitions <= kMaximumTransitionsPerDecision,
"rollout work bound failed");
State metadata = fixture;
metadata.score = 9'876'543;
metadata.level = 73;
metadata.moves_played = 812;
const Decision metadata_decision = chooseRolloutAction(metadata, config);
expect(first.action == metadata_decision.action &&
first.values == metadata_decision.values &&
first.scenario_returns == metadata_decision.scenario_returns,
"rollout used hidden score/level/move metadata");
expect(fairDepthOneAction(fixture) == fairDepthOneAction(metadata),
"fair D1 continuation used hidden metadata");
State mirrored = publicState(fixture);
mirrored.board = cfpi::detail::mirrorBoard(fixture.board);
const Decision mirrored_decision = chooseRolloutAction(mirrored, config);
expect(mirrored_decision.action == kBoardSize - 1 - first.action,
"root action was not reflection equivariant");
for (int column = 0; column < kBoardSize; ++column) {
expect(mirrored_decision.values[kBoardSize - 1 - column] ==
first.values[column],
"root values were not reflection equivariant");
expect(mirrored_decision.scenario_returns[kBoardSize - 1 - column] ==
first.scenario_returns[column],
"aligned return vectors were not reflection equivariant");
}
std::array<int, kBoardSize> visible_counts{};
constexpr std::uint32_t tape_seed = 0x1234'5678u;
for (int scenario = 0; scenario < kScenarios; ++scenario) {
++visible_counts[visibleDisc(tape_seed, scenario, 0) - 1];
}
for (const int count : visible_counts) {
expect(count == 1, "first future visible disc was not stratified");
}
RevealTape aligned_left{tape_seed, 2, 3, kRevealTapeDomain, 0};
RevealTape aligned_right{tape_seed, 2, 3, kRevealTapeDomain, 0};
for (int event = 0; event < 12; ++event) {
expect(aligned_left.nextDisc() == aligned_right.nextDisc(),
"sibling reveal tapes were not aligned");
}
State reveal_fixture;
reveal_fixture.board.fill(kEmpty);
reveal_fixture.board[indexOf(6, 1)] = kCracked;
reveal_fixture.next_disc = 1;
reveal_fixture.moves_remaining = 4;
MoveResult standard;
expect(playSyntheticMove(reveal_fixture, 0, tape_seed, 0, 0, standard),
"domain fixture transition failed");
bool found_visible_change = false;
bool found_reveal_change = false;
for (std::uint32_t salt = 1; salt < 256; ++salt) {
MoveResult changed_visible;
expect(playSyntheticMove(
reveal_fixture, 0, tape_seed, 0, 0, changed_visible, nullptr,
{kRevealTapeDomain, kVisibleTapeDomain ^ salt}),
"visible-domain transition failed");
if (changed_visible.state.next_disc != standard.state.next_disc) {
expect(changed_visible.state.board == standard.state.board &&
changed_visible.score_delta == standard.score_delta &&
changed_visible.waves.size() == standard.waves.size(),
"visible domain leaked into reveal mechanics");
found_visible_change = true;
}
MoveResult changed_reveal;
expect(playSyntheticMove(
reveal_fixture, 0, tape_seed, 0, 0, changed_reveal, nullptr,
{kRevealTapeDomain ^ salt, kVisibleTapeDomain}),
"reveal-domain transition failed");
if (changed_reveal.state.board != standard.state.board) {
expect(changed_reveal.state.next_disc == standard.state.next_disc,
"reveal domain leaked into visible-disc tape");
found_reveal_change = true;
}
}
expect(found_visible_change && found_reveal_change,
"domain separation fixture was not discriminating");
expect(samePublicState(publicState(metadata), fixture),
"public-state normalization failed");
}
void writeSummary(std::ostream& output, const Summary& summary) {
output << "{\"games\":" << summary.games
<< ",\"censored\":" << summary.censored
<< ",\"correctedMeanScore\":" << summary.mean_score
<< ",\"medianScore\":" << summary.median_score
<< ",\"lowerQuartileScore\":" << summary.lower_quartile_score
<< ",\"minimumScore\":" << summary.minimum_score
<< ",\"maximumScore\":" << summary.maximum_score
<< ",\"standardError\":" << summary.standard_error
<< ",\"meanMoves\":" << summary.mean_moves
<< ",\"numberedClearsPerMove\":" << summary.clears_per_move
<< ",\"coversRevealedPerMove\":" << summary.reveals_per_move
<< ",\"meanDecisionMs\":" << summary.mean_decision_ms
<< ",\"meanTransitionsPerDecision\":"
<< summary.mean_transitions_per_decision
<< ",\"totalMoves\":" << summary.total_moves
<< ",\"totalNumberedClears\":" << summary.total_clears
<< ",\"totalCoversRevealed\":" << summary.total_reveals << '}';
}
void writeGames(std::ostream& output, const std::vector<GameResult>& games) {
output << '[';
for (std::size_t index = 0; index < games.size(); ++index) {
if (index > 0) output << ',';
const GameResult& game = games[index];
output << "{\"seed\":" << game.seed << ",\"score\":" << game.score
<< ",\"moves\":" << game.moves
<< ",\"numberedCleared\":" << game.numbered_cleared
<< ",\"coversRevealed\":" << game.covers_revealed
<< ",\"clearBoards\":" << game.clear_boards
<< ",\"maximumChain\":" << game.maximum_chain
<< ",\"censored\":" << (game.censored ? "true" : "false")
<< ",\"decisionSeconds\":" << game.decision_seconds
<< ",\"transitions\":" << game.transitions
<< ",\"fairD1Calls\":" << game.fair_d1_calls << '}';
}
output << ']';
}
struct Options {
std::string output = "/tmp/drop7-fair-d1-rollout-improvement.json";
int threads = kDefaultThreads;
bool self_test_only = false;
};
Options parseOptions(int argc, char** argv) {
Options result;
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--self-test-only") {
result.self_test_only = true;
continue;
}
if (index + 1 >= argc) throw std::invalid_argument("missing option value");
if (argument == "--output") {
result.output = argv[++index];
} else if (argument == "--threads") {
result.threads = std::stoi(argv[++index]);
if (result.threads < 1 || result.threads > 64) {
throw std::invalid_argument("threads must be in [1,64]");
}
} else {
throw std::invalid_argument("unknown option " + argument);
}
}
return result;
}
int run(const Options& options) {
runSelfTests();
std::cerr << "fair-d1-rollout self-tests passed\n";
if (options.self_test_only) return 0;
const auto total_started = Clock::now();
// Complete and report this baseline before fitting the rollout policy.
const std::vector<GameResult> fitting_baseline = runCohort(
kFittingStart, kFittingGames, Policy::kFairD1, nullptr,
options.threads, "fitting-fair-d1");
const Summary fitting_baseline_summary = summarize(fitting_baseline);
std::cerr << "fair-d1 benchmark corrected mean "
<< fitting_baseline_summary.mean_score << " clears/move "
<< fitting_baseline_summary.clears_per_move << '\n';
std::array<std::vector<GameResult>, kConfigs.size()> fitting_candidates;
std::array<Summary, kConfigs.size()> fitting_summaries{};
for (std::size_t index = 0; index < kConfigs.size(); ++index) {
std::ostringstream label;
label << "fitting-h" << kConfigs[index].horizon << "-tail"
<< kConfigs[index].tail_scale;
fitting_candidates[index] = runCohort(
kFittingStart, kFittingGames, Policy::kRollout, &kConfigs[index],
options.threads, label.str());
fitting_summaries[index] = summarize(fitting_candidates[index]);
}
const int champion = selectConfig(fitting_summaries);
const Summary& champion_summary =
fitting_summaries[static_cast<std::size_t>(champion)];
const LeaveOneOut loo =
leaveOneOut(fitting_baseline, fitting_candidates, champion);
const bool complete = fitting_baseline_summary.censored == 0 &&
champion_summary.censored == 0;
const bool score_gate = champion_summary.mean_score >= kMinimumFittingMean;
const bool throughput_gate =
champion_summary.clears_per_move >=
fitting_baseline_summary.clears_per_move *
kMinimumClearThroughputRatio;
const bool fitting_gate =
complete && score_gate && throughput_gate && loo.gains_survive;
std::vector<GameResult> heldout_baseline;
std::vector<GameResult> heldout_candidate;
Summary heldout_baseline_summary;
Summary heldout_candidate_summary;
bool heldout_passed = false;
if (fitting_gate) {
heldout_baseline = runCohort(kHeldoutStart, kHeldoutGames,
Policy::kFairD1, nullptr, options.threads,
"heldout-fair-d1");
heldout_candidate = runCohort(
kHeldoutStart, kHeldoutGames, Policy::kRollout,
&kConfigs[static_cast<std::size_t>(champion)], options.threads,
"heldout-frozen-rollout");
heldout_baseline_summary = summarize(heldout_baseline);
heldout_candidate_summary = summarize(heldout_candidate);
heldout_passed =
heldout_baseline_summary.censored == 0 &&
heldout_candidate_summary.censored == 0 &&
heldout_candidate_summary.mean_score >
heldout_baseline_summary.mean_score &&
heldout_candidate_summary.clears_per_move >=
heldout_baseline_summary.clears_per_move *
kMinimumClearThroughputRatio;
}
const double total_wall =
std::chrono::duration<double>(Clock::now() - total_started).count();
std::ofstream output(options.output);
if (!output) throw std::runtime_error("could not open output artifact");
output << std::setprecision(12)
<< "{\n \"experiment\":\"fair-d1-rollout-improvement\",\n"
<< " \"preregistered\":true,\n"
<< " \"publicStateOnly\":true,\n"
<< " \"strategyFusionFree\":true,\n"
<< " \"meanReturnSelection\":true,\n"
<< " \"cvarUsed\":false,\n"
<< " \"scoring\":{\"levelBonus\":7000},\n"
<< " \"seedDiscipline\":{\"fittingStart\":" << kFittingStart
<< ",\"fittingGames\":" << kFittingGames
<< ",\"heldoutStart\":" << kHeldoutStart
<< ",\"heldoutGames\":" << kHeldoutGames
<< ",\"forbiddenFamilies\":[\"0x3e\",\"0x7d\",\"0xd7\"]},\n"
<< " \"tapes\":{\"scenarios\":" << kScenarios
<< ",\"alignedAcrossRootActions\":true,"
"\"eventIndexed\":true,\"revealVisibleDomainsSeparate\":true,"
"\"futureTapeVisibleToContinuation\":false,"
"\"firstFutureDiscExactlyStratified\":true},\n"
<< " \"continuation\":{\"policy\":\"fair-depth-one\","
"\"publicStateOnly\":true},\n"
<< " \"grid\":[";
for (std::size_t index = 0; index < kConfigs.size(); ++index) {
if (index > 0) output << ',';
output << "{\"horizon\":" << kConfigs[index].horizon
<< ",\"tailScale\":" << kConfigs[index].tail_scale << '}';
}
output << "],\n \"resourceBound\":{\"maximumMoves\":" << kMaximumMoves
<< ",\"maximumTransitionsPerDecision\":"
<< kMaximumTransitionsPerDecision
<< ",\"threads\":" << options.threads << "},\n"
<< " \"gate\":{\"minimumCorrectedFittingMean\":"
<< kMinimumFittingMean
<< ",\"minimumClearThroughputRatio\":"
<< kMinimumClearThroughputRatio
<< ",\"minimumLeaveOneOutWins\":" << kMinimumLooScoreWins
<< "},\n \"fittingFairD1\":{\"summary\":";
writeSummary(output, fitting_baseline_summary);
output << ",\"games\":";
writeGames(output, fitting_baseline);
output << "},\n \"fittingCandidates\":[";
for (std::size_t index = 0; index < kConfigs.size(); ++index) {
if (index > 0) output << ',';
output << "{\"configIndex\":" << index << ",\"summary\":";
writeSummary(output, fitting_summaries[index]);
output << ",\"games\":";
writeGames(output, fitting_candidates[index]);
output << '}';
}
output << "],\n \"selection\":{\"championIndex\":" << champion
<< ",\"horizon\":"
<< kConfigs[static_cast<std::size_t>(champion)].horizon
<< ",\"tailScale\":"
<< kConfigs[static_cast<std::size_t>(champion)].tail_scale
<< ",\"scoreGate\":" << (score_gate ? "true" : "false")
<< ",\"throughputGate\":"
<< (throughput_gate ? "true" : "false")
<< ",\"completeGameGate\":" << (complete ? "true" : "false")
<< ",\"fittingGatePassed\":"
<< (fitting_gate ? "true" : "false") << "},\n"
<< " \"leaveOneOut\":{\"winnerCounts\":[";
for (std::size_t index = 0; index < loo.winner_counts.size(); ++index) {
if (index > 0) output << ',';
output << loo.winner_counts[index];
}
output << "],\"globalChampionScoreWins\":" << loo.global_score_wins
<< ",\"globalChampionThroughputWins\":"
<< loo.global_throughput_wins
<< ",\"globalChampionMeanRange\":[" << loo.global_mean_min << ','
<< loo.global_mean_max << "],\"gainsSurvive\":"
<< (loo.gains_survive ? "true" : "false") << "},\n"
<< " \"heldoutRan\":" << (fitting_gate ? "true" : "false")
<< ",\n \"heldout\":";
if (!fitting_gate) {
output << "null";
} else {
output << "{\"baselineSummary\":";
writeSummary(output, heldout_baseline_summary);
output << ",\"candidateSummary\":";
writeSummary(output, heldout_candidate_summary);
output << ",\"passed\":" << (heldout_passed ? "true" : "false")
<< ",\"baselineGames\":";
writeGames(output, heldout_baseline);
output << ",\"candidateGames\":";
writeGames(output, heldout_candidate);
output << '}';
}
output << ",\n \"qualified\":"
<< (fitting_gate && heldout_passed ? "true" : "false")
<< ",\n \"totalWallSeconds\":" << total_wall
<< ",\n \"peakRssBytes\":" << peakRssBytes() << "\n}\n";
output.close();
std::cout << std::setprecision(10)
<< "fair D1 mean=" << fitting_baseline_summary.mean_score
<< " clears/move=" << fitting_baseline_summary.clears_per_move
<< '\n'
<< "champion=h"
<< kConfigs[static_cast<std::size_t>(champion)].horizon
<< " tail="
<< kConfigs[static_cast<std::size_t>(champion)].tail_scale
<< " mean=" << champion_summary.mean_score
<< " clears/move=" << champion_summary.clears_per_move
<< " loo=" << loo.global_score_wins << '/'
<< kFittingGames << " score, " << loo.global_throughput_wins
<< '/' << kFittingGames << " throughput\n"
<< "fitting gate=" << (fitting_gate ? "pass" : "fail")
<< " heldout="
<< (fitting_gate ? (heldout_passed ? "pass" : "fail")
: "not-run")
<< " artifact=" << options.output << '\n';
return fitting_gate && heldout_passed ? 0 : 2;
}
} // namespace drop7::fair_d1_rollout_improvement
#ifndef DROP7_FAIR_D1_ROLLOUT_IMPROVEMENT_LIBRARY
int main(int argc, char** argv) {
try {
const auto options =
drop7::fair_d1_rollout_improvement::parseOptions(argc, argv);
return drop7::fair_d1_rollout_improvement::run(options);
} catch (const std::exception& error) {
std::cerr << "drop7_fair_d1_rollout_improvement: " << error.what()
<< '\n';
return 1;
}
}
#endif