C++ engine
The reference engine: it plays the fair depth-3 and depth-4 reference games and nearly every large research run, and every other engine is proven against it.
On this page
Design
The C++ engine is the stable research baseline. It keeps the same 49-cell, row-major board as the TypeScript rules and translates those operations into plain native loops. The goal is a shared meaning for every move, with enough throughput for full-game cohorts and search experiments.
Implementation
A close port of the readable rules
The state, scoring constants and wave record mirror the TypeScript types. During a cascade, the engine records every popper before it changes any gray disc, consumes reveals in row-major order, and applies gravity after the complete wave. The comments call out that ordering because later chain waves can observe it.
inline void resolveCascade(Board& board, Mulberry32& random, int starting_depth,
std::int64_t& score,
std::vector<Wave>& waves) {
for (int depth = starting_depth;; ++depth) {
int popper_count = 0;
const auto poppers = findPoppers(board, popper_count);
if (popper_count == 0) return;
std::array<bool, kCellCount> popping{};
Board cleared = board;
for (int offset = 0; offset < popper_count; ++offset) {
const int index = poppers[offset];
popping[index] = true;
cleared[index] = kEmpty;
}
std::array<int, kCellCount> reveals{};
int reveal_count = 0;
constexpr std::array<std::array<int, 2>, 4> directions{{
{{-1, 0}}, {{1, 0}}, {{0, -1}}, {{0, 1}},
}};
for (int row = 0; row < kBoardSize; ++row) {
for (int column = 0; column < kBoardSize; ++column) {
const int index = indexOf(row, column);
const std::uint8_t cell = board[index];
if (cell != kSolid && cell != kCracked) continue;
int hits = 0;
for (const auto& direction : directions) {
const int neighbor_row = row + direction[0];
const int neighbor_column = column + direction[1];
if (inside(neighbor_row, neighbor_column) &&
popping[indexOf(neighbor_row, neighbor_column)]) {
++hits;
}
}
if (hits == 0) continue;
const int hits_needed = cell == kSolid ? 2 : 1;
if (hits >= hits_needed) {
reveals[reveal_count++] = index;
} else {
cleared[index] = kCracked;
}
}
}
// engine.ts scans the board in row-major order and consumes reveal values
// before gravity. The ordering is observable through subsequent chains.
for (int offset = 0; offset < reveal_count; ++offset) {
cleared[reveals[offset]] = random.nextDisc();
}
const std::int64_t points = popper_count * scoreForWave(depth);
score += points;
waves.push_back({depth, popper_count, reveal_count, points});
board = applyGravity(cleared);
}Reproducible games across languages
A headless game derives the next visible disc and each move's reveal stream from separate domains of the same seed. That lets the TypeScript and C++ drivers receive the same randomness without depending on how many random calls a policy makes between moves.
inline bool playHeadlessMove(State& state, std::uint32_t game_seed, int column,
MoveResult& result) {
const std::uint32_t reveal_seed =
mix32(game_seed ^
(static_cast<std::uint32_t>(state.moves_played + 1) *
0x85eb'ca6bu) ^
kRevealDomain);
Mulberry32 random(reveal_seed);
if (!playMove(state, column, random, result)) return false;
state = result.state;
if (!state.game_over) {
state.next_disc = headlessDisc(game_seed, state.moves_played);
}
return true;A separate sampled move loop
Research search does not draw one random future. The policy layer supplies stratified chance samples and a templated random source. It carries a second copy of the cascade and move loop so the sample order and floating-point accumulation remain fixed.
inline double stratifiedUnit(std::uint32_t seed, int sample, int count,
std::uint32_t domain, int event) {
const std::uint32_t event_seed = mix32(
seed ^ domain ^
(static_cast<std::uint32_t>(event + 1) * kDepthMultiplier));
const int rotation = static_cast<int>(event_seed %
static_cast<std::uint32_t>(count));
const int stratum = (sample + rotation) % count;
const double jitter = static_cast<double>(
mix32(event_seed ^
(static_cast<std::uint32_t>(sample + 1) * kSampleMultiplier))) /
4'294'967'296.0;
return (static_cast<double>(stratum) + jitter) /
static_cast<double>(count);
}
struct StratifiedRandom {
std::uint32_t seed = 0;
int sample = 0;
int count = 1;
int event = 0;
std::uint8_t nextDisc() {
const double unit = stratifiedUnit(seed, sample, count,
kRevealSampleDomain, event++);
return static_cast<std::uint8_t>(
std::floor(unit * static_cast<double>(kBoardSize)) + 1.0);
}
};Uses
The fair depth-3 and depth-4 policy is built directly on this engine. The native suite, neural training environments and most C++ experiment entry points share it through the common whole-game harness. It is also the anchor for every alternative engine's trajectory gate.
Verification
The TypeScript parity run matched 256 games and 6,852 moves exactly (reproducibility guide). The scenario, fast C++ and Rust engines each add independent move-for-move replays against this implementation.
Technical recordThe independent trajectory gates anchored to the C++ reference
- Scenario engine: 8,192 game-plays and 218,470 moves, with no mismatch (finding-02).
- Fast C++ engine: 8,288 games, 438,020 moves and 548,263 waves, with no mismatch (finding-13).
- Rust engine: three trajectory arms covering 36,427 moves and 40,286 waves, with no mismatch (RS-20260824T075451Z-e89ea128).
Limits
The sampled policy loop duplicates the move rules and has indirect search-parity coverage, while no focused gate compares it with playMove directly (docs/exploratory/audit-01-engine-fidelity.md). Compiler settings are also part of the scientific result: the efficiency audit found leaf-value changes from floating-point contraction, then restored bit identity by disabling contraction (docs/exploratory/audit-06-engine-efficiency.md).
Agent contextBuild flags, parity entry points and the duplicated-loop risk
The pinned source is src/core/native/engine.hpp; the sampled policy path is in src/core/native/public-behavior.hpp. Preserve reveal order, column order and floating-point accumulation. Native comparison builds use clang++ with -ffp-contract=off where bit parity is required.
Run make test-native and make parity before accepting a semantic change. A future direct gate should drive identical explicit random values through playMove and playMoveSampled and compare the complete move record.