Scenario engine and exact solver
Fix every future disc and hidden number in advance so a position has one exactly computable best line.
On this page
No linked records
Design
The base engine chooses a gray disc's number when that disc opens. It therefore has no persistent hidden board that can be fixed and solved as one puzzle. The scenario engine pairs every covered cell with a hidden value, fixes the future disc tape and future risen rows, then asks for the best line through that complete future. The result is an oracle diagnostic that reads information a playable policy cannot see.
Implementation
Board and hidden values move together
Gravity and row rises apply the same permutation to two arrays: the visible board and its latent values. The visible transform calls the shared C++ rule, while the paired loop moves each hidden value with its gray disc.
// The board result is produced by the shared primitive `drop7::applyGravity`;
// only the latent permutation is computed here, with the identical loop shape,
// so the two arrays can never drift apart. `scenario-parity.cpp` additionally
// asserts the permutation agrees with `drop7::applyGravity` on random boards.
inline void applyGravityPaired(const Board& board, const LatentBoard& latent,
Board& out_board, LatentBoard& out_latent) {
out_latent.fill(0);
for (int column = 0; column < kBoardSize; ++column) {
int destination = kBoardSize - 1;
for (int row = kBoardSize - 1; row >= 0; --row) {
const int index = indexOf(row, column);
if (board[index] == kEmpty) continue;
out_latent[indexOf(destination--, column)] = latent[index];
}
}
out_board = applyGravity(board);
}
// Board legality and the row shift come from `drop7::raiseCoveredRow`.
inline bool raiseCoveredRowPaired(const Board& board, const LatentBoard& latent,
const RiseRow& rise_latent, Board& out_board,
LatentBoard& out_latent) {
Board raised{};
if (!raiseCoveredRow(board, raised)) return false;
out_latent.fill(0);
for (int row = 0; row < kBoardSize - 1; ++row) {
for (int column = 0; column < kBoardSize; ++column) {
out_latent[indexOf(row, column)] = latent[indexOf(row + 1, column)];
}
}
for (int column = 0; column < kBoardSize; ++column) {
out_latent[indexOf(kBoardSize - 1, column)] = rise_latent[column];
}
out_board = raised;
return true;Two reveal sources, one move loop
Stream mode draws at reveal time and reproduces the base engine. Latent mode reads the value already attached to the covered cell, advances an explicit disc tape, and supplies a fixed hidden row on each rise. Both feed the same templated cascade.
// Reproduces `drop7::playMove` exactly: reveal values and the next visible disc
// both come from one Mulberry32 stream, in the engine's consumption order.
struct StreamRevealSource {
Mulberry32* random = nullptr;
std::uint8_t reveal(int /*index*/, const LatentBoard& /*latent*/) {
return random->nextDisc();
}
std::uint8_t nextVisibleDisc() { return random->nextDisc(); }
RiseRow riseValues() { return RiseRow{}; } // latent unused in stream mode
bool exhausted() const { return false; }
};
// Consumes the fixed hidden value that already sits under each covered cell.
// Cells introduced by a row rise take their values from `rise_rows`, and the
// visible disc sequence is read from `tape`.
struct LatentRevealSource {
const std::uint8_t* tape = nullptr;
int tape_length = 0;
int tape_index = 0;
const RiseRow* rise_rows = nullptr;
int rise_count = 0;
int rise_index = 0;
bool tape_exhausted = false;
bool rise_exhausted = false;
bool invalid_latent = false;
std::uint8_t reveal(int index, const LatentBoard& latent) {
const std::uint8_t value = latent[index];
if (value < 1 || value > kBoardSize) {
invalid_latent = true;
return 1;
}
return value;
}
// Beyond the tape the scenario is undefined; callers stop at the horizon, so
// a 0 here marks "no further disc is specified" rather than a legal disc.
std::uint8_t nextVisibleDisc() {
if (tape_index >= tape_length) {
tape_exhausted = true;
return 0;
}
return tape[tape_index++];
}
RiseRow riseValues() {
if (rise_index >= rise_count) {
rise_exhausted = true;
return RiseRow{};
}
return rise_rows[rise_index++];
}
bool exhausted() const { return tape_exhausted || rise_exhausted; }
};A small exact state
At a fixed search depth, the tape index, rise index and moves to the next rise are already known. A search node therefore needs only the visible board and latent board. The solver can memoize that pair plus depth and return the exact best remaining score inside the scenario's horizon.
// Applies one move of a scenario at search depth `depth`. Everything the
// reveal source needs is a function of the depth alone: the tape index, the
// rise index, and the number of moves left before the next rise. This is what
// makes a scenario node exactly (board, latent, depth).
inline bool applyScenarioMove(const Scenario& scenario, const SearchNode& node,
int depth, int column, SearchNode& child,
std::int64_t& delta, bool& game_over,
MoveResult& scratch) {
State state;
state.board = node.board;
state.next_disc = scenario.disc_tape[static_cast<std::size_t>(depth)];
state.score = 0;
state.level = 1;
state.moves_remaining = movesRemainingAt(scenario.moves_remaining, depth);
state.moves_played = depth;
state.game_over = false;
LatentRevealSource source;
source.tape = scenario.disc_tape.data();
source.tape_length = static_cast<int>(scenario.disc_tape.size());
source.tape_index = depth + 1;
source.rise_rows = scenario.rise_latent.data();
source.rise_count = static_cast<int>(scenario.rise_latent.size());
source.rise_index = risesConsumed(depth, scenario.moves_remaining);
if (!playScenarioMove(state, node.latent, column, source, scratch,
child.latent)) {
return false;
}
child.board = scratch.state.board;
delta = scratch.score_delta;
game_over = scratch.state.game_over;
return true;
}Uses
The solver can label a fixed position with its best action, measure how far a shallow policy is from the known optimum, and test whether a proposed bound is admissible. The scenario suite also checks exact-solver infrastructure. Since it sees hidden values and the full future tape, its scores belong to oracle analysis and cannot be compared with public-policy game scores.
Verification
Stream mode replayed 8,192 game-plays and 218,470 moves against the C++ engine with no mismatch (finding-02). The optimized exact solver was also compared with a deliberately naive enumerator on 107 scenarios and four solver variants, for 428 matching comparisons from the same finding.
Technical recordThe parity and exactness checks for the scenario instrument
- Whole trajectories compare boards, scores, move clocks, terminal state and every wave.
- Paired gravity and row-rise transforms are checked independently on generated boards.
- Latent scenarios validate that every covered cell has one hidden value and that JSONL round trips preserve it.
- The exact solver runs with its table, bound and thread settings crossed against naive enumeration.
The recorded command is build/scenario/scenario-parity --seeds 4096; details and retained results are in finding-02.
Limits
Fixing a hidden board changes the game's dynamics even though each individual hidden value has the same uniform marginal distribution as an on-reveal draw. The oracle's answer is exact only for its recorded tape and horizon. The retained benchmark also found that its admissible score bound pruned no nodes at the tested horizons; the transposition table supplied the useful reduction (finding-02).
Agent contextInformation boundary, scenario invariants and extension commands
Treat every latent scenario, optimum and principal variation as oracle data. Do not expose hidden values, tape entries or scenario identity to a deployable policy. A student may train on an oracle label only under a registered teacher protocol and must be frozen before public-interface evaluation.
Scenario IDs hash the visible board, latent board, move clock, horizon, disc tape and future rise rows. Run approaches/lifetime-objective/scenario/build.sh, then build/scenario/solve --self-testand the parity command above after changing the record shape, paired transforms or solver.