// Exploratory bounded correction around fair D4: make the frozen leaf's
// nineteen weights runtime data instead of compile-time constants, prove that
// the frozen weight vector reproduces the reference bit-for-bit, and then test
// the reweightings that docs/exploratory/finding-10-suite-validation.md
// Addendum A says predict achievable clears far better than the frozen vector.
//
// Motivation. Addendum A labelled 1,024 public positions with their exact
// 8-move achievable clear optimum averaged over independent completions of the
// hidden board and the future. Predicting that label, occupancy alone reaches
// held-out R^2 0.187, the frozen scalar fairLeaf(state) reaches 0.396, the
// leaf's own nineteen features freely reweighted reach 0.734, and every linear
// model over all 53 candidate structural properties tops out at 0.753.
// Reweighting recovers 95% of all the signal any structural property can
// supply, and the cosine between the frozen weight direction and the
// predictive direction is +0.141. The leaf is not missing information; its
// weights point almost orthogonally to it.
//
// This program changes no existing source. The search driver is a copy of
// approaches/lifetime-objective/risk-calibration/search.cpp, which is already
// proved decision-identical to the frozen reference; the only change is that
// frozen::fairLeaf is replaced by a local dot product over
// frozen::extractFairFeatures with runtime weights. The feature extractor,
// chance stratification, canonicalization, cache keying, column order and work
// accounting all come from the unmodified frozen code, so the ONLY degree of
// freedom exposed here is the weight vector.
//
// CHECK gate (--leaf-check, --parity, --self-parity):
// 1. with the frozen weight vector the local leaf must return values whose
// raw uint64 bit patterns are identical to frozen::fairLeaf on a large
// sample of real boards drawn from fair play;
// 2. at default parameters the driver must select the same column as the
// unmodified reference on every move of every probe game; and
// 3. at any parameterization the driver with frozen weights must produce
// identical actions, scores, moves and work counts to the same driver
// compiled against frozen::fairLeaf directly.
#include "fair-only-depth4-noentry.cpp"
#include "../../../approaches/lifetime-objective/common/harness.hpp"
#include <cstring>
#include <exception>
#include <limits>
#include <list>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
namespace drop7::lifetime::leafw {
namespace ref = drop7::fair_only_depth4;
namespace frozen = drop7::fair_only_horizon;
// ---------------------------------------------------------------------------
// The leaf, with its weights as data.
// ---------------------------------------------------------------------------
// fair-only-horizon.cpp:53-71, in the exact order fairLeaf accumulates them.
// The order is load-bearing: floating point addition is not associative, so
// bit-identity with the frozen leaf requires the same accumulation order.
constexpr int kLeafTerms = 19;
constexpr const char* kLeafNames[kLeafTerms] = {
"open_columns", "height_load",
"solid_cells", "cracked_cells",
"numbered_cells", "high_low_numbers",
"direct_potential", "latent_chain_potential",
"cracked_exposure", "solid_exposure",
"adjacent_ones", "triple_twos",
"dead_low_numbers", "covered_height_risk",
"low_number_height_risk", "danger_height_squared",
"roughness", "rise_pressure",
"next_disc_vertical_options",
};
constexpr double kFrozenWeights[kLeafTerms] = {
frozen::kOpenColumnsWeight,
frozen::kHeightLoadWeight,
frozen::kSolidCellsWeight,
frozen::kCrackedCellsWeight,
frozen::kNumberedCellsWeight,
frozen::kHighLowNumbersWeight,
frozen::kDirectPotentialWeight,
frozen::kLatentChainPotentialWeight,
frozen::kCrackedExposureWeight,
frozen::kSolidExposureWeight,
frozen::kAdjacentOnesWeight,
frozen::kTripleTwosWeight,
frozen::kDeadLowNumbersWeight,
frozen::kCoveredHeightRiskWeight,
frozen::kLowNumberHeightRiskWeight,
frozen::kDangerHeightSquaredWeight,
frozen::kRoughnessWeight,
frozen::kRisePressureWeight,
frozen::kNextDiscVerticalOptionsWeight,
};
// Guard against the frozen constants moving underneath this experiment.
static_assert(kFrozenWeights[0] == 180.0);
static_assert(kFrozenWeights[4] == -18.0);
static_assert(kFrozenWeights[9] == 40.0);
static_assert(kFrozenWeights[8] == 100.0);
static_assert(kFrozenWeights[13] == -95.0);
static_assert(kFrozenWeights[16] == 0.0);
static_assert(kFrozenWeights[17] == -35.0);
struct LeafWeights {
double w[kLeafTerms] = {
kFrozenWeights[0], kFrozenWeights[1], kFrozenWeights[2],
kFrozenWeights[3], kFrozenWeights[4], kFrozenWeights[5],
kFrozenWeights[6], kFrozenWeights[7], kFrozenWeights[8],
kFrozenWeights[9], kFrozenWeights[10], kFrozenWeights[11],
kFrozenWeights[12], kFrozenWeights[13], kFrozenWeights[14],
kFrozenWeights[15], kFrozenWeights[16], kFrozenWeights[17],
kFrozenWeights[18]};
// A constant offset. It is not part of the frozen leaf and defaults to
// exactly zero, but a refit vector is only comparable to the frozen one if
// its level as well as its scale can be matched, and the level matters
// because leaf points are compared against a -1,000,000 terminal utility.
double bias = 0.0;
bool isFrozen() const {
if (bias != 0.0) return false;
for (int i = 0; i < kLeafTerms; ++i) {
if (w[i] != kFrozenWeights[i]) return false;
}
return true;
}
};
// Identical in structure to frozen::fairLeaf (fair-only-horizon.cpp:135-162);
// the constants are replaced by weights.w[] in the same positions and the
// accumulation order is preserved exactly.
inline double parameterizedLeaf(const State& state, const LeafWeights& weights) {
if (state.game_over) return frozen::kFairTerminalUtility;
const frozen::FairFeatures features = frozen::extractFairFeatures(state);
const auto& f = features.heuristic;
double result = 0.0;
result += weights.w[0] * f.open_columns;
result += weights.w[1] * f.height_load;
result += weights.w[2] * f.solid_cells;
result += weights.w[3] * f.cracked_cells;
result += weights.w[4] * f.numbered_cells;
result += weights.w[5] * f.high_low_numbers;
result += weights.w[6] * f.direct_potential;
result += weights.w[7] * f.latent_chain_potential;
result += weights.w[8] * f.cracked_exposure;
result += weights.w[9] * f.solid_exposure;
result += weights.w[10] * f.adjacent_ones;
result += weights.w[11] * f.triple_twos;
result += weights.w[12] * f.dead_low_numbers;
result += weights.w[13] * features.covered_height_risk;
result += weights.w[14] * features.low_number_height_risk;
result += weights.w[15] * features.danger_height_squared;
result += weights.w[16] * features.roughness;
result += weights.w[17] * features.rise_pressure;
result += weights.w[18] * features.next_disc_vertical_options;
if (weights.bias != 0.0) result += weights.bias;
return result;
}
inline std::uint64_t bits(double value) {
std::uint64_t raw = 0;
std::memcpy(&raw, &value, sizeof raw);
return raw;
}
// ---------------------------------------------------------------------------
// The search. Copied verbatim from
// approaches/lifetime-objective/risk-calibration/search.cpp except that
// evaluateLeaf calls parameterizedLeaf instead of frozen::fairLeaf.
// ---------------------------------------------------------------------------
struct SearchParameters {
int depth = 4;
int chanceSamples = frozen::kChanceSamples;
double terminalUtility = frozen::kTerminalUtility;
std::uint64_t maximumWork = 3'200'000;
std::size_t maximumCacheEntries = 60'000;
LeafWeights weights{};
};
class WorkLimitReached : public std::exception {};
struct CacheEntry {
double value = 0.0;
std::list<std::string>::iterator order;
};
struct SearchContext {
std::unordered_map<std::string, CacheEntry> cache;
std::list<std::string> order;
std::uint64_t nodes = 0;
std::uint64_t work = 0;
std::uint64_t cacheHits = 0;
};
class ParameterizedSearch {
public:
explicit ParameterizedSearch(SearchParameters parameters)
: parameters_(parameters) {}
int chooseAction(const State& source, std::uint64_t& work) {
if (source.game_over) return -1;
bool mirrored = false;
const State canonical = cfpi::detail::canonicalState(source, mirrored);
SearchContext context;
int action = -1;
for (int depth = 1; depth <= parameters_.depth; ++depth) {
try {
const int candidate = rootDecision(canonical, depth, context);
if (candidate < 0) break;
action = candidate;
} catch (const WorkLimitReached&) {
break;
}
}
if (action < 0) action = centerFirstMove(canonical.board);
work += context.work;
return mirrored && action >= 0 ? kBoardSize - 1 - action : action;
}
private:
void checkBudget(const SearchContext& context) const {
if (context.work >= parameters_.maximumWork) throw WorkLimitReached{};
}
void cacheValue(SearchContext& context, std::string key, double value) const {
const auto prior = context.cache.find(key);
if (prior != context.cache.end()) {
context.order.erase(prior->second.order);
context.cache.erase(prior);
}
while (context.cache.size() >= parameters_.maximumCacheEntries) {
const std::string& oldest = context.order.front();
context.cache.erase(oldest);
context.order.pop_front();
}
context.order.push_back(std::move(key));
const auto order = std::prev(context.order.end());
context.cache.emplace(*order, CacheEntry{value, order});
}
double evaluateAction(const State& state, int column, int depth,
SearchContext& context) const {
const std::uint32_t stateSeed = cfpi::detail::scenarioSeedForState(
state, frozen::kPolicySeed, depth);
double value = 0.0;
for (int sample = 0; sample < parameters_.chanceSamples; ++sample) {
checkBudget(context);
cfpi::detail::StratifiedRandom random{stateSeed, sample,
parameters_.chanceSamples, 0};
MoveResult move;
const bool played =
cfpi::detail::playMoveSampled(state, column, random, move);
++context.work;
if (!played) {
value += parameters_.terminalUtility;
continue;
}
const double scoreDelta = static_cast<double>(move.score_delta);
if (move.state.game_over) {
value += scoreDelta + parameters_.terminalUtility;
continue;
}
move.state.score = 0;
move.state.next_disc = cfpi::detail::sampledNextDisc(
stateSeed, sample, parameters_.chanceSamples);
bool ignored = false;
const State next = cfpi::detail::canonicalState(move.state, ignored);
value += scoreDelta + bestFutureValue(next, depth - 1, context);
}
return value / parameters_.chanceSamples;
}
double evaluateLeaf(const State& state, SearchContext& context) const {
checkBudget(context);
++context.work;
const double value = parameterizedLeaf(state, parameters_.weights);
if (!std::isfinite(value)) {
throw std::runtime_error("leaf returned a non-finite value");
}
return value;
}
double bestFutureValue(const State& state, int depth,
SearchContext& context) const {
++context.nodes;
checkBudget(context);
if (state.game_over) return parameters_.terminalUtility;
if (depth == 0) return evaluateLeaf(state, context);
const std::string key = cfpi::detail::dynamicStateKey(state, depth);
const auto cached = context.cache.find(key);
if (cached != context.cache.end()) {
++context.cacheHits;
const double value = cached->second.value;
context.order.splice(context.order.end(), context.order,
cached->second.order);
return value;
}
double best = -std::numeric_limits<double>::infinity();
for (const int column : cfpi::detail::kColumnOrder) {
if (!isLegal(state.board, column)) continue;
best = std::max(best, evaluateAction(state, column, depth, context));
}
if (!std::isfinite(best)) best = parameters_.terminalUtility;
cacheValue(context, key, best);
return best;
}
int rootDecision(const State& canonical, int depth,
SearchContext& context) const {
int action = -1;
double bestValue = -std::numeric_limits<double>::infinity();
for (const int column : cfpi::detail::kColumnOrder) {
if (!isLegal(canonical.board, column)) continue;
const double value = evaluateAction(canonical, column, depth, context);
if (value > bestValue) {
bestValue = value;
action = column;
}
}
return action;
}
SearchParameters parameters_;
};
// The same driver, hard-wired to frozen::fairLeaf. It exists only so that the
// CHECK gate can compare "weights as data" against "weights as constants"
// inside one process at an arbitrary parameterization, including the d4/s7
// configuration the reference driver cannot be asked for.
class FrozenLeafSearch {
public:
explicit FrozenLeafSearch(SearchParameters parameters)
: parameters_(parameters) {}
int chooseAction(const State& source, std::uint64_t& work) {
if (source.game_over) return -1;
bool mirrored = false;
const State canonical = cfpi::detail::canonicalState(source, mirrored);
SearchContext context;
int action = -1;
for (int depth = 1; depth <= parameters_.depth; ++depth) {
try {
const int candidate = rootDecision(canonical, depth, context);
if (candidate < 0) break;
action = candidate;
} catch (const WorkLimitReached&) {
break;
}
}
if (action < 0) action = centerFirstMove(canonical.board);
work += context.work;
return mirrored && action >= 0 ? kBoardSize - 1 - action : action;
}
private:
void checkBudget(const SearchContext& context) const {
if (context.work >= parameters_.maximumWork) throw WorkLimitReached{};
}
void cacheValue(SearchContext& context, std::string key, double value) const {
const auto prior = context.cache.find(key);
if (prior != context.cache.end()) {
context.order.erase(prior->second.order);
context.cache.erase(prior);
}
while (context.cache.size() >= parameters_.maximumCacheEntries) {
const std::string& oldest = context.order.front();
context.cache.erase(oldest);
context.order.pop_front();
}
context.order.push_back(std::move(key));
const auto order = std::prev(context.order.end());
context.cache.emplace(*order, CacheEntry{value, order});
}
double evaluateAction(const State& state, int column, int depth,
SearchContext& context) const {
const std::uint32_t stateSeed = cfpi::detail::scenarioSeedForState(
state, frozen::kPolicySeed, depth);
double value = 0.0;
for (int sample = 0; sample < parameters_.chanceSamples; ++sample) {
checkBudget(context);
cfpi::detail::StratifiedRandom random{stateSeed, sample,
parameters_.chanceSamples, 0};
MoveResult move;
const bool played =
cfpi::detail::playMoveSampled(state, column, random, move);
++context.work;
if (!played) {
value += parameters_.terminalUtility;
continue;
}
const double scoreDelta = static_cast<double>(move.score_delta);
if (move.state.game_over) {
value += scoreDelta + parameters_.terminalUtility;
continue;
}
move.state.score = 0;
move.state.next_disc = cfpi::detail::sampledNextDisc(
stateSeed, sample, parameters_.chanceSamples);
bool ignored = false;
const State next = cfpi::detail::canonicalState(move.state, ignored);
value += scoreDelta + bestFutureValue(next, depth - 1, context);
}
return value / parameters_.chanceSamples;
}
double bestFutureValue(const State& state, int depth,
SearchContext& context) const {
++context.nodes;
checkBudget(context);
if (state.game_over) return parameters_.terminalUtility;
if (depth == 0) {
checkBudget(context);
++context.work;
return frozen::fairLeaf(state);
}
const std::string key = cfpi::detail::dynamicStateKey(state, depth);
const auto cached = context.cache.find(key);
if (cached != context.cache.end()) {
++context.cacheHits;
const double value = cached->second.value;
context.order.splice(context.order.end(), context.order,
cached->second.order);
return value;
}
double best = -std::numeric_limits<double>::infinity();
for (const int column : cfpi::detail::kColumnOrder) {
if (!isLegal(state.board, column)) continue;
best = std::max(best, evaluateAction(state, column, depth, context));
}
if (!std::isfinite(best)) best = parameters_.terminalUtility;
cacheValue(context, key, best);
return best;
}
int rootDecision(const State& canonical, int depth,
SearchContext& context) const {
int action = -1;
double bestValue = -std::numeric_limits<double>::infinity();
for (const int column : cfpi::detail::kColumnOrder) {
if (!isLegal(canonical.board, column)) continue;
const double value = evaluateAction(canonical, column, depth, context);
if (value > bestValue) {
bestValue = value;
action = column;
}
}
return action;
}
SearchParameters parameters_;
};
// ---------------------------------------------------------------------------
// CHECK gate 1 - raw bit-pattern identity of the leaf on real boards.
// ---------------------------------------------------------------------------
struct LeafCheckResult {
std::uint64_t boards = 0;
std::uint64_t mismatches = 0;
std::uint64_t terminalBoards = 0;
double minimumLeaf = 0.0;
double maximumLeaf = 0.0;
double meanLeaf = 0.0;
};
// Walks real fair-play trajectories and, at every visited state, compares the
// parameterized leaf against frozen::fairLeaf on that state and on every state
// reachable by one legal drop under every stratified chance sample - i.e. on
// exactly the population of boards the search's leaf actually sees.
LeafCheckResult leafBitCheck(const SearchParameters& parameters,
std::uint32_t seedStart, int games,
int maximumMoves, std::ostream& out) {
LeafCheckResult result;
const LeafWeights frozenWeights{};
if (!frozenWeights.isFrozen()) throw std::runtime_error("default weights moved");
double total = 0.0;
bool first = true;
ParameterizedSearch search{parameters};
for (int game = 0; game < games; ++game) {
const std::uint32_t seed = seedStart + static_cast<std::uint32_t>(game);
State state = initialHeadlessState(seed);
int moves = 0;
while (!state.game_over && moves < maximumMoves) {
auto compare = [&](const State& probe) {
const double mine = parameterizedLeaf(probe, frozenWeights);
const double theirs = frozen::fairLeaf(probe);
++result.boards;
if (probe.game_over) ++result.terminalBoards;
if (bits(mine) != bits(theirs)) {
if (result.mismatches < 5) {
out << " leaf mismatch seed 0x" << std::hex << seed << " mine "
<< bits(mine) << " frozen " << bits(theirs) << std::dec << '\n';
}
++result.mismatches;
}
total += mine;
if (first || mine < result.minimumLeaf) result.minimumLeaf = mine;
if (first || mine > result.maximumLeaf) result.maximumLeaf = mine;
first = false;
};
compare(state);
for (int column = 0; column < kBoardSize; ++column) {
if (!isLegal(state.board, column)) continue;
const std::uint32_t stateSeed = cfpi::detail::scenarioSeedForState(
state, frozen::kPolicySeed, parameters.depth);
for (int sample = 0; sample < parameters.chanceSamples; ++sample) {
cfpi::detail::StratifiedRandom random{
stateSeed, sample, parameters.chanceSamples, 0};
MoveResult move;
if (!cfpi::detail::playMoveSampled(state, column, random, move)) continue;
move.state.score = 0;
move.state.next_disc = cfpi::detail::sampledNextDisc(
stateSeed, sample, parameters.chanceSamples);
bool ignored = false;
const State next = cfpi::detail::canonicalState(move.state, ignored);
if (next.game_over) continue;
compare(next);
// one more ply, so the sample includes deeper, more crowded boards
for (int column2 = 0; column2 < kBoardSize; ++column2) {
if (!isLegal(next.board, column2)) continue;
cfpi::detail::StratifiedRandom random2{
stateSeed, sample, parameters.chanceSamples, 1};
MoveResult move2;
if (!cfpi::detail::playMoveSampled(next, column2, random2, move2)) continue;
move2.state.score = 0;
move2.state.next_disc = cfpi::detail::sampledNextDisc(
stateSeed, sample, parameters.chanceSamples);
bool ignored2 = false;
const State next2 = cfpi::detail::canonicalState(move2.state, ignored2);
if (next2.game_over) continue;
compare(next2);
}
}
}
std::uint64_t work = 0;
int column = search.chooseAction(state, work);
if (column < 0 || !isLegal(state.board, column)) {
column = centerFirstMove(state.board);
if (column < 0) break;
}
MoveResult move;
if (!playHeadlessMove(state, seed, column, move)) break;
++moves;
}
}
result.meanLeaf = result.boards > 0
? total / static_cast<double>(result.boards)
: 0.0;
return result;
}
// ---------------------------------------------------------------------------
// CHECK gate 2 - the frozen reference driver, at its own configuration.
// ---------------------------------------------------------------------------
bool referenceParityCheck(std::uint32_t seedStart, int games, int maximumMoves,
std::ostream& out) {
ParameterizedSearch mine{SearchParameters{}};
std::uint64_t mismatches = 0;
std::uint64_t comparedMoves = 0;
for (int game = 0; game < games; ++game) {
const std::uint32_t seed = seedStart + static_cast<std::uint32_t>(game);
State state = initialHeadlessState(seed);
while (!state.game_over && state.moves_played < maximumMoves) {
const ref::SearchDecision reference = ref::chooseDepth4Action(state);
std::uint64_t work = 0;
const int candidate = mine.chooseAction(state, work);
++comparedMoves;
if (candidate != reference.action) {
++mismatches;
out << " mismatch seed 0x" << std::hex << seed << std::dec << " move "
<< state.moves_played << ": reference " << reference.action
<< " parameterized " << candidate << '\n';
}
MoveResult move;
if (!playHeadlessMove(state, seed, reference.action, move)) break;
}
}
out << "reference-parity: " << comparedMoves << " moves compared, "
<< mismatches << " mismatches\n";
return mismatches == 0;
}
// ---------------------------------------------------------------------------
// CHECK gate 3 - weights-as-data against weights-as-constants at the actual
// evaluation configuration, comparing columns AND cumulative work.
// ---------------------------------------------------------------------------
bool selfParityCheck(const SearchParameters& parameters,
std::uint32_t seedStart, int games, int maximumMoves,
std::ostream& out) {
ParameterizedSearch mine{parameters};
FrozenLeafSearch theirs{parameters};
std::uint64_t mismatches = 0;
std::uint64_t workMismatches = 0;
std::uint64_t comparedMoves = 0;
std::uint64_t totalWork = 0;
for (int game = 0; game < games; ++game) {
const std::uint32_t seed = seedStart + static_cast<std::uint32_t>(game);
State state = initialHeadlessState(seed);
int moves = 0;
std::uint64_t mineWork = 0, theirWork = 0;
while (!state.game_over && moves < maximumMoves) {
const std::uint64_t beforeMine = mineWork, beforeTheirs = theirWork;
const int a = mine.chooseAction(state, mineWork);
const int b = theirs.chooseAction(state, theirWork);
++comparedMoves;
if (a != b) {
++mismatches;
if (mismatches < 5) {
out << " self mismatch seed 0x" << std::hex << seed << std::dec
<< " move " << moves << ": data " << a << " constants " << b
<< '\n';
}
}
if (mineWork - beforeMine != theirWork - beforeTheirs) {
++workMismatches;
if (workMismatches < 5) {
out << " work mismatch seed 0x" << std::hex << seed << std::dec
<< " move " << moves << ": data " << (mineWork - beforeMine)
<< " constants " << (theirWork - beforeTheirs) << '\n';
}
}
int column = a;
if (column < 0 || !isLegal(state.board, column)) {
column = centerFirstMove(state.board);
if (column < 0) break;
}
MoveResult move;
if (!playHeadlessMove(state, seed, column, move)) break;
++moves;
}
totalWork += mineWork;
}
out << "self-parity: " << comparedMoves << " moves compared, " << mismatches
<< " column mismatches, " << workMismatches << " work mismatches, "
<< totalWork << " total work\n";
return mismatches == 0 && workMismatches == 0;
}
// ---------------------------------------------------------------------------
// Options.
// ---------------------------------------------------------------------------
int leafIndexOf(const std::string& name) {
for (int i = 0; i < kLeafTerms; ++i) {
if (name == kLeafNames[i]) return i;
}
return -1;
}
struct Options {
CohortOptions cohort;
SearchParameters parameters;
std::string output;
std::string arm = "frozen";
bool referenceParity = false;
bool selfParity = false;
bool leafCheck = false;
int checkGames = 4;
int checkMoves = 40;
};
void applyWeightsFile(SearchParameters& parameters, const std::string& path) {
std::ifstream file(path);
if (!file) throw std::runtime_error("cannot open weights file " + path);
std::string name;
double value = 0.0;
int applied = 0;
while (file >> name) {
if (name.size() && name[0] == '#') {
std::string rest;
std::getline(file, rest);
continue;
}
if (!(file >> value)) {
throw std::runtime_error("weights file: no value for " + name);
}
if (name == "bias") {
parameters.weights.bias = value;
++applied;
continue;
}
const int index = leafIndexOf(name);
if (index < 0) throw std::runtime_error("unknown leaf term " + name);
parameters.weights.w[index] = value;
++applied;
}
if (applied == 0) throw std::runtime_error("weights file applied nothing: " + path);
}
Options parseOptions(int argc, char** argv) {
Options options;
for (int index = 1; index < argc;) {
const std::string key = argv[index];
if (key == "--reference-parity") {
options.referenceParity = true;
index += 1;
continue;
}
if (key == "--self-parity") {
options.selfParity = true;
index += 1;
continue;
}
if (key == "--leaf-check") {
options.leafCheck = true;
index += 1;
continue;
}
if (index + 1 >= argc) throw std::invalid_argument("missing value for " + key);
const std::string value = argv[index + 1];
if (key == "--seed-start") {
options.cohort.seedStart =
static_cast<std::uint32_t>(std::stoul(value, nullptr, 0));
} else if (key == "--games") {
options.cohort.games = std::stoi(value);
} else if (key == "--max-moves") {
options.cohort.maximumMoves = std::stoi(value);
} else if (key == "--threads") {
options.cohort.threads = std::stoi(value);
} else if (key == "--depth") {
options.parameters.depth = std::stoi(value);
} else if (key == "--chance-samples") {
options.parameters.chanceSamples = std::stoi(value);
} else if (key == "--terminal-utility") {
options.parameters.terminalUtility = std::stod(value);
} else if (key == "--max-work") {
options.parameters.maximumWork = std::stoull(value);
} else if (key == "--weights") {
applyWeightsFile(options.parameters, value);
} else if (key == "--weight") {
const auto split = value.find('=');
if (split == std::string::npos) {
throw std::invalid_argument("--weight expects name=value");
}
const std::string name = value.substr(0, split);
const double amount = std::stod(value.substr(split + 1));
if (name == "bias") {
options.parameters.weights.bias = amount;
} else {
const int i = leafIndexOf(name);
if (i < 0) throw std::invalid_argument("unknown leaf term " + name);
options.parameters.weights.w[i] = amount;
}
} else if (key == "--arm") {
options.arm = value;
} else if (key == "--check-games") {
options.checkGames = std::stoi(value);
} else if (key == "--check-moves") {
options.checkMoves = std::stoi(value);
} else if (key == "--output") {
options.output = value;
} else {
throw std::invalid_argument("unknown option " + key);
}
index += 2;
}
return options;
}
std::string configJson(const Options& options) {
std::ostringstream config;
config << std::setprecision(12) << "{\"arm\": \"" << options.arm
<< "\", \"depth\": " << options.parameters.depth
<< ", \"chanceSamples\": " << options.parameters.chanceSamples
<< ", \"terminalUtility\": " << options.parameters.terminalUtility
<< ", \"maximumWork\": " << options.parameters.maximumWork
<< ", \"frozenWeights\": "
<< (options.parameters.weights.isFrozen() ? "true" : "false")
<< ", \"weights\": {";
for (int i = 0; i < kLeafTerms; ++i) {
if (i) config << ", ";
config << '"' << kLeafNames[i] << "\": " << options.parameters.weights.w[i];
}
config << ", \"bias\": " << options.parameters.weights.bias << "}}";
return config.str();
}
} // namespace drop7::lifetime::leafw
int main(int argc, char** argv) {
using namespace drop7;
using namespace drop7::lifetime;
try {
const auto options = leafw::parseOptions(argc, argv);
if (options.leafCheck) {
const auto result = leafw::leafBitCheck(
options.parameters, options.cohort.seedStart, options.checkGames,
options.checkMoves, std::cout);
std::cout << "leaf-check: " << result.boards << " boards compared, "
<< result.mismatches << " bit mismatches, leaf range ["
<< std::setprecision(12) << result.minimumLeaf << ", "
<< result.maximumLeaf << "], mean " << result.meanLeaf << '\n';
std::cout << (result.mismatches == 0 ? "LEAF BITS IDENTICAL\n"
: "LEAF BITS DIFFER\n");
return result.mismatches == 0 ? 0 : 1;
}
if (options.referenceParity) {
const bool ok = leafw::referenceParityCheck(
options.cohort.seedStart, options.checkGames, options.checkMoves,
std::cout);
std::cout << (ok ? "REFERENCE PARITY OK\n" : "REFERENCE PARITY FAILED\n");
return ok ? 0 : 1;
}
if (options.selfParity) {
const bool ok = leafw::selfParityCheck(
options.parameters, options.cohort.seedStart, options.checkGames,
options.checkMoves, std::cout);
std::cout << (ok ? "SELF PARITY OK\n" : "SELF PARITY FAILED\n");
return ok ? 0 : 1;
}
const auto started = std::chrono::steady_clock::now();
auto records = runCohort(options.cohort, [&]() {
return [search = leafw::ParameterizedSearch{options.parameters}](
const State& state, std::uint64_t& work) mutable {
return search.chooseAction(state, work);
};
});
const double wall = std::chrono::duration<double>(
std::chrono::steady_clock::now() - started)
.count();
const std::string config = leafw::configJson(options);
if (options.output.empty()) {
writeArtifact(std::cout, "leaf-reweight-fair-search", config,
options.cohort, records, wall);
} else {
std::ofstream file(options.output);
if (!file) throw std::runtime_error("cannot open " + options.output);
writeArtifact(file, "leaf-reweight-fair-search", config, options.cohort,
records, wall);
}
return 0;
} catch (const std::exception& error) {
std::cerr << "leaf-reweight failed: " << error.what() << '\n';
return 1;
}
}