// Bit-exact port of approaches/lifetime-objective/fast-engine/fast-leaf.hpp
// (itself proven bit-identical to the frozen reference fairLeaf by gate-leaf).
// Every floating-point expression is copied in the same order with the same
// accumulator types; nothing is re-associated. The gate compares uint64 bit
// patterns of the returned values against the C++ build over real boards.
use crate::board::{BOARD_SIZE, CELL_COUNT, CRACKED, EMPTY, SOLID};
use crate::engine::State;
use crate::tables::RUN_TABLE;
pub mod weights {
pub const FAIR_TERMINAL_UTILITY: f64 = -2_500_000.0;
pub const OPEN_COLUMNS: f64 = 180.0;
pub const HEIGHT_LOAD: f64 = -20.0;
pub const SOLID_CELLS: f64 = -620.0;
pub const CRACKED_CELLS: f64 = -220.0;
pub const NUMBERED_CELLS: f64 = -18.0;
pub const HIGH_LOW_NUMBERS: f64 = -90.0;
pub const DIRECT_POTENTIAL: f64 = 1_600.0;
pub const LATENT_CHAIN_POTENTIAL: f64 = 700.0;
pub const CRACKED_EXPOSURE: f64 = 100.0;
pub const SOLID_EXPOSURE: f64 = 40.0;
pub const ADJACENT_ONES: f64 = -550.0;
pub const TRIPLE_TWOS: f64 = -750.0;
pub const DEAD_LOW_NUMBERS: f64 = -120.0;
pub const COVERED_HEIGHT_RISK: f64 = -95.0;
pub const LOW_NUMBER_HEIGHT_RISK: f64 = -85.0;
pub const DANGER_HEIGHT_SQUARED: f64 = -1_250.0;
// kRoughnessWeight is 0.0 in the reference; the term is removed (L7).
pub const RISE_PRESSURE: f64 = -35.0;
pub const NEXT_DISC_VERTICAL_OPTIONS: f64 = 220.0;
}
pub const LEAF_TERM_NAMES: [&str; 18] = [
"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",
"rise_pressure",
"next_disc_vertical_options",
];
pub const FROZEN_LEAF_WEIGHTS: [f64; 18] = [
weights::OPEN_COLUMNS,
weights::HEIGHT_LOAD,
weights::SOLID_CELLS,
weights::CRACKED_CELLS,
weights::NUMBERED_CELLS,
weights::HIGH_LOW_NUMBERS,
weights::DIRECT_POTENTIAL,
weights::LATENT_CHAIN_POTENTIAL,
weights::CRACKED_EXPOSURE,
weights::SOLID_EXPOSURE,
weights::ADJACENT_ONES,
weights::TRIPLE_TWOS,
weights::DEAD_LOW_NUMBERS,
weights::COVERED_HEIGHT_RISK,
weights::LOW_NUMBER_HEIGHT_RISK,
weights::DANGER_HEIGHT_SQUARED,
weights::RISE_PRESSURE,
weights::NEXT_DISC_VERTICAL_OPTIONS,
];
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LeafWeights {
pub values: [f64; 18],
}
impl LeafWeights {
pub const fn frozen() -> Self {
Self {
values: FROZEN_LEAF_WEIGHTS,
}
}
pub fn is_frozen(&self) -> bool {
self.values
.iter()
.zip(FROZEN_LEAF_WEIGHTS.iter())
.all(|(left, right)| left.to_bits() == right.to_bits())
}
/// Read the same strict "name value" format used by the C++ weighted
/// leaf. Every one of the eighteen terms must occur exactly once.
pub fn read_named(path: &str) -> Result<Self, String> {
let text = std::fs::read_to_string(path)
.map_err(|error| format!("cannot read leaf weights {path}: {error}"))?;
let mut result = Self::frozen();
let mut seen = [false; 18];
for (line_number, source) in text.lines().enumerate() {
let line = source.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() != 2 {
return Err(format!(
"{path}:{}: expected one leaf name and one value",
line_number + 1
));
}
let Some(index) = LEAF_TERM_NAMES.iter().position(|name| *name == fields[0]) else {
return Err(format!(
"{path}:{}: unknown leaf term {}",
line_number + 1,
fields[0]
));
};
if seen[index] {
return Err(format!(
"{path}:{}: duplicate leaf term {}",
line_number + 1,
fields[0]
));
}
let value: f64 = fields[1].parse().map_err(|_| {
format!(
"{path}:{}: invalid value for {}",
line_number + 1,
fields[0]
)
})?;
if !value.is_finite() {
return Err(format!(
"{path}:{}: non-finite value for {}",
line_number + 1,
fields[0]
));
}
result.values[index] = value;
seen[index] = true;
}
if let Some(index) = seen.iter().position(|present| !present) {
return Err(format!(
"{path}: missing leaf term {}",
LEAF_TERM_NAMES[index]
));
}
Ok(result)
}
}
impl Default for LeafWeights {
fn default() -> Self {
Self::frozen()
}
}
const READINESS_TABLE_SIZE: i32 = 80;
/// ldexp(1.0, 1 - cost): exact powers of two. Table lookup in the reachable
/// range; beyond it an exact repeated halving (never taken in practice, kept
/// for total correctness).
#[inline]
fn readiness(cost: i32) -> f64 {
if cost < 1 {
return 0.0;
}
if cost < READINESS_TABLE_SIZE {
// 2^(1-cost) for cost in 1..80: exponent field 1023+1-cost is normal.
return f64::from_bits(((1023 + 1 - cost) as u64) << 52);
}
let mut value = 1.0f64;
for _ in 1..cost {
value *= 0.5;
}
value
}
#[inline(always)]
fn union_readiness(first: f64, second: f64) -> f64 {
1.0 - (1.0 - first) * (1.0 - second)
}
/// minimumHorizontalAdditionCostFast: integer arithmetic only, identical to
/// the C++ difference-of-prefix-sums search.
#[allow(clippy::too_many_arguments)]
fn minimum_horizontal_addition_cost(
value: i32,
segment_start: i32,
segment_end: i32,
segment_length: i32,
heights: &[i32; BOARD_SIZE],
elevation: i32,
prefix: &[i32; BOARD_SIZE + 1],
) -> f64 {
if segment_start < 0 || value <= segment_length {
return -1.0;
}
let mut best = i32::MAX;
let lowest = (segment_end - value + 1).max(0);
let highest = (segment_start).min(BOARD_SIZE as i32 - value);
let mut start = lowest;
while start <= highest {
let end = start + value - 1;
if start > 0 && heights[(start - 1) as usize] >= elevation {
start += 1;
continue;
}
if (end + 1) < BOARD_SIZE as i32 && heights[(end + 1) as usize] >= elevation {
start += 1;
continue;
}
let cost = prefix[(end + 1) as usize] - prefix[start as usize];
if cost > 0 {
best = best.min(cost);
}
start += 1;
}
if best == i32::MAX {
-1.0
} else {
best as f64
}
}
/// Descending insertion sort over at most seven doubles; the reference reads
/// one order statistic of the sorted values, a property of the multiset.
fn sort_descending(values: &mut [f64]) {
for index in 1..values.len() {
let key = values[index];
let mut position = index;
while position > 0 && values[position - 1] < key {
values[position] = values[position - 1];
position -= 1;
}
values[position] = key;
}
}
fn release_readiness(excess: i32, support: &mut [f64], count: usize) -> f64 {
if excess <= 0 || (count as i32) < excess {
return 0.0;
}
sort_descending(&mut support[..count]);
support[(excess - 1) as usize] * readiness(excess)
}
/// Per-leaf scratch buffers (stack/struct allocated once per searcher).
pub struct LeafScratch {
pub heights: [i32; BOARD_SIZE],
pub row_mask: [u8; BOARD_SIZE],
pub column_mask: [u8; BOARD_SIZE],
pub twos_row: [u8; BOARD_SIZE],
pub twos_column: [u8; BOARD_SIZE],
pub present_bits: u64,
pub cover_bits: u64,
pub ones_bits: u64,
pub addition: [f64; CELL_COUNT],
pub release: [f64; CELL_COUNT],
pub horizontal_addition: [f64; CELL_COUNT],
pub vertical_addition: [f64; CELL_COUNT],
pub horizontal_release: [f64; CELL_COUNT],
pub vertical_release: [f64; CELL_COUNT],
}
impl Default for LeafScratch {
fn default() -> Self {
LeafScratch {
heights: [0; BOARD_SIZE],
row_mask: [0; BOARD_SIZE],
column_mask: [0; BOARD_SIZE],
twos_row: [0; BOARD_SIZE],
twos_column: [0; BOARD_SIZE],
present_bits: 0,
cover_bits: 0,
ones_bits: 0,
addition: [0.0; CELL_COUNT],
release: [0.0; CELL_COUNT],
horizontal_addition: [0.0; CELL_COUNT],
vertical_addition: [0.0; CELL_COUNT],
horizontal_release: [0.0; CELL_COUNT],
vertical_release: [0.0; CELL_COUNT],
}
}
}
impl LeafScratch {
#[inline(always)]
fn present(&self, index: usize) -> bool {
(self.present_bits >> index) & 1 != 0
}
}
#[derive(Default)]
struct Features {
open_columns: i32,
height_load: f64,
solid_cells: i32,
cracked_cells: i32,
numbered_cells: i32,
high_low_numbers: i32,
direct_potential: f64,
latent_chain_potential: f64,
cracked_exposure: f64,
solid_exposure: f64,
adjacent_ones: f64,
triple_twos: f64,
dead_low_numbers: f64,
covered_height_risk: f64,
low_number_height_risk: f64,
danger_height_squared: f64,
rise_pressure: f64,
next_disc_vertical_options: f64,
}
/// The eighteen leaf terms in `LEAF_TERM_NAMES` order, before any weight is
/// applied. This is stages 1-6 of the reference leaf (`leaf_features`),
/// which `weighted_fair_leaf` also calls, so the terms are exactly the
/// quantities the frozen leaf dots with its constants; the
/// `terms_dot_frozen_weights_match_fair_leaf_bits` test reconstructs the
/// frozen leaf from them bit-for-bit. A game-over state is not special-cased
/// here: callers that need the terminal utility check `state.game_over`.
pub fn fair_leaf_terms(state: &State, scratch: &mut LeafScratch) -> [f64; 18] {
let f = leaf_features(state, scratch);
[
f.open_columns as f64,
f.height_load,
f.solid_cells as f64,
f.cracked_cells as f64,
f.numbered_cells as f64,
f.high_low_numbers as f64,
f.direct_potential,
f.latent_chain_potential,
f.cracked_exposure,
f.solid_exposure,
f.adjacent_ones,
f.triple_twos,
f.dead_low_numbers,
f.covered_height_risk,
f.low_number_height_risk,
f.danger_height_squared,
f.rise_pressure,
f.next_disc_vertical_options,
]
}
/// Stages 1-6 of the reference leaf: every feature, computed in the
/// reference order. Extracted unchanged from `weighted_fair_leaf` so that
/// `fair_leaf_terms` can expose the terms; the weighted leaf's arithmetic is
/// the same code it always was.
fn leaf_features(state: &State, scratch: &mut LeafScratch) -> Features {
// The leaf reads cells many times in row-major order; unpack the packed
// column words to the byte view once so every read below is a
// direct byte load, the same access cost the C++ leaf pays.
let cells = state.board.to_bytes();
let board = &state.board;
let mut f = Features::default();
// --- stage 1: occupancy masks, heights, open columns -------------------
// The packed representation yields the masks directly (measured faster
// than a byte sweep here); contents are the same as the C++ row sweep.
let (row_mask, col_mask) = board.scan_masks();
scratch.row_mask = row_mask;
scratch.column_mask = col_mask;
let mut maximum_height = 0i32;
for column in 0..BOARD_SIZE {
let height = scratch.column_mask[column].count_ones() as i32;
scratch.heights[column] = height;
if cells[column] == EMPTY {
f.open_columns += 1;
}
maximum_height = maximum_height.max(height);
f.rise_pressure += ((height * height * height) as f64) / state.moves_remaining as f64;
if height < BOARD_SIZE as i32 && height + 1 == state.next_disc as i32 {
f.next_disc_vertical_options += 1.0;
}
}
let danger = (maximum_height - 4).max(0);
f.danger_height_squared = (danger * danger) as f64;
scratch.present_bits = 0;
scratch.cover_bits = 0;
scratch.ones_bits = 0;
scratch.twos_row = [0; BOARD_SIZE];
scratch.twos_column = [0; BOARD_SIZE];
// --- stage 2: per-cell sweep -------------------------------------------
for row in 0..BOARD_SIZE {
let elevation = (BOARD_SIZE - row) as i32;
let mut prefix = [0i32; BOARD_SIZE + 1];
for column in 0..BOARD_SIZE {
prefix[column + 1] = prefix[column] + (elevation - scratch.heights[column]).max(0);
}
let horizontal = &RUN_TABLE[scratch.row_mask[row] as usize];
for column in 0..BOARD_SIZE {
let index = row * BOARD_SIZE + column;
let cell = cells[row * BOARD_SIZE + column];
if cell == EMPTY {
continue;
}
f.height_load += (elevation * elevation) as f64;
let edge_multiplier = if column == 0 || column == BOARD_SIZE - 1 {
1.65
} else {
1.0
};
if cell == SOLID || cell == CRACKED {
if cell == SOLID {
f.solid_cells += 1;
f.covered_height_risk += (elevation * elevation) as f64 * edge_multiplier;
} else {
f.cracked_cells += 1;
f.covered_height_risk +=
(elevation * elevation) as f64 * edge_multiplier * 0.72;
}
scratch.cover_bits |= 1u64 << index;
continue;
}
if !(1..=7).contains(&cell) {
continue;
}
f.numbered_cells += 1;
if cell <= 2 {
let height_risk = (elevation - 2).max(0);
f.low_number_height_risk += (height_risk * height_risk) as f64;
if elevation >= 5 {
f.high_low_numbers += 1;
}
}
scratch.present_bits |= 1u64 << index;
if cell == 1 {
scratch.ones_bits |= 1u64 << index;
} else if cell == 2 {
scratch.twos_row[row] |= 1u8 << column;
scratch.twos_column[column] |= 1u8 << row;
}
let height = scratch.heights[column];
let vertical_addition = if cell as i32 > height {
readiness(cell as i32 - height)
} else {
0.0
};
let horizontal_cost = minimum_horizontal_addition_cost(
cell as i32,
horizontal.start[column] as i32,
horizontal.end[column] as i32,
horizontal.length[column] as i32,
&scratch.heights,
elevation,
&prefix,
);
let horizontal_addition = if horizontal_cost < 0.0 {
0.0
} else {
readiness(horizontal_cost as i32)
};
let addition = union_readiness(horizontal_addition, vertical_addition);
scratch.vertical_addition[index] = vertical_addition;
scratch.horizontal_addition[index] = horizontal_addition;
scratch.addition[index] = addition;
f.direct_potential += addition;
}
}
// --- stage 3: release inventory ----------------------------------------
let mut remaining = scratch.present_bits;
while remaining != 0 {
let index = remaining.trailing_zeros() as usize;
remaining &= remaining - 1;
let row = index / BOARD_SIZE;
let column = index % BOARD_SIZE;
let horizontal = &RUN_TABLE[scratch.row_mask[row] as usize];
let vertical = &RUN_TABLE[scratch.column_mask[column] as usize];
let value = cells[index] as i32;
let horizontal_excess = horizontal.length[column] as i32 - value;
let vertical_excess = vertical.length[row] as i32 - value;
// Release is exactly zero unless the run is longer than the disc's
// value. Avoid gathering a supporter inventory that cannot be read.
let horizontal_release = if horizontal_excess > 0 {
let mut support = [0.0f64; BOARD_SIZE];
let mut count = 0usize;
let mut scan_col = horizontal.start[column] as i32;
while scan_col <= horizontal.end[column] as i32 {
let supporter = row * BOARD_SIZE + scan_col as usize;
if supporter != index && scratch.present(supporter) {
support[count] = scratch.addition[supporter];
count += 1;
}
scan_col += 1;
}
release_readiness(horizontal_excess, &mut support, count)
} else {
0.0
};
let vertical_release = if vertical_excess > 0 {
let mut support = [0.0f64; BOARD_SIZE];
let mut count = 0usize;
let mut scan_row = vertical.start[row] as i32;
while scan_row <= vertical.end[row] as i32 {
let supporter = scan_row as usize * BOARD_SIZE + column;
if supporter != index && scratch.present(supporter) {
support[count] = scratch.addition[supporter];
count += 1;
}
scan_row += 1;
}
release_readiness(vertical_excess, &mut support, count)
} else {
0.0
};
let release = union_readiness(horizontal_release, vertical_release);
scratch.horizontal_release[index] = horizontal_release;
scratch.vertical_release[index] = vertical_release;
scratch.release[index] = release;
f.latent_chain_potential += release;
if value <= 2
&& horizontal.length[column] as i32 > value
&& vertical.length[row] as i32 > value
{
f.dead_low_numbers += 1.0 - union_readiness(scratch.addition[index], release);
}
}
// --- stage 4: adjacent ones --------------------------------------------
let mut remaining = scratch.ones_bits;
while remaining != 0 {
let index = remaining.trailing_zeros() as usize;
remaining &= remaining - 1;
let row = index / BOARD_SIZE;
let column = index % BOARD_SIZE;
if column + 1 < BOARD_SIZE && cells[index + 1] == 1 {
let escape = union_readiness(
scratch.vertical_addition[index],
scratch.vertical_release[index],
)
.max(union_readiness(
scratch.vertical_addition[index + 1],
scratch.vertical_release[index + 1],
));
f.adjacent_ones += 1.0 - escape;
}
if row + 1 < BOARD_SIZE && cells[index + BOARD_SIZE] == 1 {
let escape = union_readiness(
scratch.horizontal_addition[index],
scratch.horizontal_release[index],
)
.max(union_readiness(
scratch.horizontal_addition[index + BOARD_SIZE],
scratch.horizontal_release[index + BOARD_SIZE],
));
f.adjacent_ones += 1.0 - escape;
}
}
// --- stage 5: runs of twos ----------------------------------------------
for row in 0..BOARD_SIZE {
let mut mask = scratch.twos_row[row];
while mask != 0 {
let start = mask.trailing_zeros() as usize;
let mut run = mask >> start;
let mut length = 0usize;
while run & 1 != 0 {
length += 1;
run >>= 1;
}
let excess = length as i32 - 2;
if excess > 0 {
let mut escape = 0.0f64;
for column in start..start + length {
let slot = row * BOARD_SIZE + column;
escape = escape.max(union_readiness(
scratch.vertical_addition[slot],
scratch.vertical_release[slot],
));
}
f.triple_twos += (excess * excess) as f64 * (1.0 - escape);
}
mask &= !(((1u8 << length) - 1) << start);
}
}
for column in 0..BOARD_SIZE {
let mut mask = scratch.twos_column[column];
while mask != 0 {
let start = mask.trailing_zeros() as usize;
let mut run = mask >> start;
let mut length = 0usize;
while run & 1 != 0 {
length += 1;
run >>= 1;
}
let excess = length as i32 - 2;
if excess > 0 {
let mut escape = 0.0f64;
for row in start..start + length {
let slot = row * BOARD_SIZE + column;
escape = escape.max(union_readiness(
scratch.horizontal_addition[slot],
scratch.horizontal_release[slot],
));
}
f.triple_twos += (excess * excess) as f64 * (1.0 - escape);
}
mask &= !(((1u8 << length) - 1) << start);
}
}
// --- stage 6: cover exposure --------------------------------------------
let mut remaining = scratch.cover_bits;
while remaining != 0 {
let index = remaining.trailing_zeros() as usize;
remaining &= remaining - 1;
let row = index / BOARD_SIZE;
let column = index % BOARD_SIZE;
let mut support = [0.0f64; 4];
let mut count = 0usize;
// Neighbour order {-1,0},{1,0},{0,-1},{0,1} as in the reference.
if row > 0 && scratch.present(index - BOARD_SIZE) {
support[count] = union_readiness(
scratch.addition[index - BOARD_SIZE],
scratch.release[index - BOARD_SIZE],
);
count += 1;
}
if row + 1 < BOARD_SIZE && scratch.present(index + BOARD_SIZE) {
support[count] = union_readiness(
scratch.addition[index + BOARD_SIZE],
scratch.release[index + BOARD_SIZE],
);
count += 1;
}
if column > 0 && scratch.present(index - 1) {
support[count] =
union_readiness(scratch.addition[index - 1], scratch.release[index - 1]);
count += 1;
}
if column + 1 < BOARD_SIZE && scratch.present(index + 1) {
support[count] =
union_readiness(scratch.addition[index + 1], scratch.release[index + 1]);
count += 1;
}
if cells[index] == CRACKED {
// Retain the sorted multiplication order for exact f64 parity.
sort_descending(&mut support[..count]);
let mut inverse = 1.0f64;
for item in support.iter().take(count) {
inverse *= 1.0 - item;
}
f.cracked_exposure += 1.0 - inverse;
} else {
// Solid exposure reads only the two largest supports. All
// readiness values are finite and nonnegative; ties have the
// same bits, so selection preserves the sorted result exactly.
let mut largest = 0.0f64;
let mut second = 0.0f64;
for &value in &support[..count] {
if value > largest {
second = largest;
largest = value;
} else if value > second {
second = value;
}
}
f.solid_exposure += (if count > 0 { largest * 0.35 } else { 0.0 })
+ (if count > 1 { second * 0.65 } else { 0.0 });
}
}
f
}
/// The runtime-weighted fair leaf. At `LeafWeights::frozen()` this preserves
/// the exact operation order and is bit-identical to fastFairLeaf.
pub fn weighted_fair_leaf(
state: &State,
scratch: &mut LeafScratch,
leaf_weights: &LeafWeights,
) -> f64 {
if state.game_over {
return weights::FAIR_TERMINAL_UTILITY;
}
let f = leaf_features(state, scratch);
// The dot product, in the reference's order (L7: the roughness term is
// exactly +0.0 and is removed, as proven in the C++ header).
let mut result = 0.0f64;
if leaf_weights.is_frozen() {
// Preserve the original literal-constant code path. Besides proving
// parity, this prevents loading the runtime array from changing
// contraction/register choices in the frozen reference evaluator.
result += weights::OPEN_COLUMNS * f.open_columns as f64;
result += weights::HEIGHT_LOAD * f.height_load;
result += weights::SOLID_CELLS * f.solid_cells as f64;
result += weights::CRACKED_CELLS * f.cracked_cells as f64;
result += weights::NUMBERED_CELLS * f.numbered_cells as f64;
result += weights::HIGH_LOW_NUMBERS * f.high_low_numbers as f64;
result += weights::DIRECT_POTENTIAL * f.direct_potential;
result += weights::LATENT_CHAIN_POTENTIAL * f.latent_chain_potential;
result += weights::CRACKED_EXPOSURE * f.cracked_exposure;
result += weights::SOLID_EXPOSURE * f.solid_exposure;
result += weights::ADJACENT_ONES * f.adjacent_ones;
result += weights::TRIPLE_TWOS * f.triple_twos;
result += weights::DEAD_LOW_NUMBERS * f.dead_low_numbers;
result += weights::COVERED_HEIGHT_RISK * f.covered_height_risk;
result += weights::LOW_NUMBER_HEIGHT_RISK * f.low_number_height_risk;
result += weights::DANGER_HEIGHT_SQUARED * f.danger_height_squared;
result += weights::RISE_PRESSURE * f.rise_pressure;
result += weights::NEXT_DISC_VERTICAL_OPTIONS * f.next_disc_vertical_options;
} else {
result += leaf_weights.values[0] * f.open_columns as f64;
result += leaf_weights.values[1] * f.height_load;
result += leaf_weights.values[2] * f.solid_cells as f64;
result += leaf_weights.values[3] * f.cracked_cells as f64;
result += leaf_weights.values[4] * f.numbered_cells as f64;
result += leaf_weights.values[5] * f.high_low_numbers as f64;
result += leaf_weights.values[6] * f.direct_potential;
result += leaf_weights.values[7] * f.latent_chain_potential;
result += leaf_weights.values[8] * f.cracked_exposure;
result += leaf_weights.values[9] * f.solid_exposure;
result += leaf_weights.values[10] * f.adjacent_ones;
result += leaf_weights.values[11] * f.triple_twos;
result += leaf_weights.values[12] * f.dead_low_numbers;
result += leaf_weights.values[13] * f.covered_height_risk;
result += leaf_weights.values[14] * f.low_number_height_risk;
result += leaf_weights.values[15] * f.danger_height_squared;
result += leaf_weights.values[16] * f.rise_pressure;
result += leaf_weights.values[17] * f.next_disc_vertical_options;
}
result
}
/// The frozen fair leaf evaluator. Bit-identical to
/// drop7::fast::fastFairLeaf.
pub fn fair_leaf(state: &State, scratch: &mut LeafScratch) -> f64 {
weighted_fair_leaf(state, scratch, &LeafWeights::frozen())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::{center_first_move, play_headless_move, FullWaveSink};
/// The accessor contract: dotting the exposed terms with the frozen
/// constants, in the reference order, reproduces the frozen leaf's bits on
/// opening and mid-game boards.
#[test]
fn terms_dot_frozen_weights_match_fair_leaf_bits() {
let mut scratch = LeafScratch::default();
let mut compared = 0;
for seed in [0xa527_8000u32, 0xa527_8001, 0xa527_8002, 0xa527_8003] {
let mut state = State::initial_headless(seed);
let mut sink = FullWaveSink::new();
for _ in 0..40 {
if state.game_over {
break;
}
let terms = fair_leaf_terms(&state, &mut scratch);
let mut reconstructed = 0.0f64;
for j in 0..18 {
reconstructed += FROZEN_LEAF_WEIGHTS[j] * terms[j];
}
let reference = fair_leaf(&state, &mut scratch);
assert_eq!(
reconstructed.to_bits(),
reference.to_bits(),
"seed {seed:#x}"
);
compared += 1;
let column = center_first_move(&state.board).expect("legal column");
sink.clear();
play_headless_move(&mut state, seed, column, &mut sink).expect("legal move");
}
}
assert!(compared >= 80, "compared only {compared} boards");
}
/// Compare every term with the pre-optimization evaluator, including
/// transient boards with holes that cannot be reached between moves.
/// No game seeds or trajectories are opened by this constructed matrix.
#[test]
fn optimized_terms_match_baseline_on_constructed_boards() {
use crate::board::Board;
let mut boards = vec![Board::empty(), Board::initial()];
for value in 1..=9u32 {
boards.push(Board {
cols: [value * 0x0111_1111; BOARD_SIZE],
});
for index in 0..CELL_COUNT {
let mut board = Board::empty();
board.cols[index % BOARD_SIZE] = value << (4 * (6 - index / BOARD_SIZE));
boards.push(board);
}
}
for pattern in 0..512usize {
let mut board = Board::empty();
for column in 0..BOARD_SIZE {
let height = (pattern + 3 * column) % 8;
for nibble in 0..BOARD_SIZE {
let occupied = match pattern % 4 {
0 => nibble < height, // bottom-packed
1 => (pattern + column + nibble) % 3 == 0, // sparse holes
2 => (pattern + 2 * column + nibble) % 5 != 0, // dense holes
_ => true, // full mixed board
};
if occupied {
let value = 1 + ((pattern * 13 + column * 5 + nibble * 7) % 9) as u32;
board.cols[column] |= value << (4 * nibble);
}
}
}
boards.push(board);
}
let mut optimized = LeafScratch::default();
let mut reference = LeafScratch::default();
let mut compared = 0;
for (board_index, &board) in boards.iter().enumerate() {
for next_disc in 1..=7 {
for moves_remaining in 1..=5 {
let state = State {
board,
next_disc,
moves_remaining,
game_over: false,
score: 0,
level: 0,
moves_played: 0,
};
let actual = fair_leaf_terms(&state, &mut optimized);
let expected = baseline_leaf_terms(&state, &mut reference);
for term in 0..LEAF_TERM_NAMES.len() {
assert_eq!(actual[term].to_bits(), expected[term].to_bits(),
"board {board_index}, disc {next_disc}, rise {moves_remaining}, term {}",
LEAF_TERM_NAMES[term]);
}
compared += 1;
}
}
}
assert_eq!(compared, 33_740);
}
// Frozen test-only snapshot of leaf_features at
// 7adb6b36412b4bbef25623183dac1a905570cd1a. Keep its eager supporter
// gathering and full cover sort as an independent regression oracle.
fn baseline_leaf_terms(state: &State, scratch: &mut LeafScratch) -> [f64; 18] {
let f = baseline_leaf_features(state, scratch);
[
f.open_columns as f64,
f.height_load,
f.solid_cells as f64,
f.cracked_cells as f64,
f.numbered_cells as f64,
f.high_low_numbers as f64,
f.direct_potential,
f.latent_chain_potential,
f.cracked_exposure,
f.solid_exposure,
f.adjacent_ones,
f.triple_twos,
f.dead_low_numbers,
f.covered_height_risk,
f.low_number_height_risk,
f.danger_height_squared,
f.rise_pressure,
f.next_disc_vertical_options,
]
}
fn baseline_leaf_features(state: &State, scratch: &mut LeafScratch) -> Features {
// Decode independently of the optimized Board::to_bytes implementation.
let cells: [u8; CELL_COUNT] =
std::array::from_fn(|index| state.board.get(index / BOARD_SIZE, index % BOARD_SIZE));
let board = &state.board;
let mut f = Features::default();
// --- stage 1: occupancy masks, heights, open columns -------------------
// The packed representation yields the masks directly (measured faster
// than a byte sweep here); contents are the same as the C++ row sweep.
let (row_mask, col_mask) = board.scan_masks();
scratch.row_mask = row_mask;
scratch.column_mask = col_mask;
let mut maximum_height = 0i32;
for column in 0..BOARD_SIZE {
let height = scratch.column_mask[column].count_ones() as i32;
scratch.heights[column] = height;
if cells[column] == EMPTY {
f.open_columns += 1;
}
maximum_height = maximum_height.max(height);
f.rise_pressure += ((height * height * height) as f64) / state.moves_remaining as f64;
if height < BOARD_SIZE as i32 && height + 1 == state.next_disc as i32 {
f.next_disc_vertical_options += 1.0;
}
}
let danger = (maximum_height - 4).max(0);
f.danger_height_squared = (danger * danger) as f64;
scratch.present_bits = 0;
scratch.cover_bits = 0;
scratch.ones_bits = 0;
scratch.twos_row = [0; BOARD_SIZE];
scratch.twos_column = [0; BOARD_SIZE];
// --- stage 2: per-cell sweep -------------------------------------------
for row in 0..BOARD_SIZE {
let elevation = (BOARD_SIZE - row) as i32;
let mut prefix = [0i32; BOARD_SIZE + 1];
for column in 0..BOARD_SIZE {
prefix[column + 1] = prefix[column] + (elevation - scratch.heights[column]).max(0);
}
let horizontal = &RUN_TABLE[scratch.row_mask[row] as usize];
for column in 0..BOARD_SIZE {
let index = row * BOARD_SIZE + column;
let cell = cells[row * BOARD_SIZE + column];
if cell == EMPTY {
continue;
}
f.height_load += (elevation * elevation) as f64;
let edge_multiplier = if column == 0 || column == BOARD_SIZE - 1 {
1.65
} else {
1.0
};
if cell == SOLID || cell == CRACKED {
if cell == SOLID {
f.solid_cells += 1;
f.covered_height_risk += (elevation * elevation) as f64 * edge_multiplier;
} else {
f.cracked_cells += 1;
f.covered_height_risk +=
(elevation * elevation) as f64 * edge_multiplier * 0.72;
}
scratch.cover_bits |= 1u64 << index;
continue;
}
if !(1..=7).contains(&cell) {
continue;
}
f.numbered_cells += 1;
if cell <= 2 {
let height_risk = (elevation - 2).max(0);
f.low_number_height_risk += (height_risk * height_risk) as f64;
if elevation >= 5 {
f.high_low_numbers += 1;
}
}
scratch.present_bits |= 1u64 << index;
if cell == 1 {
scratch.ones_bits |= 1u64 << index;
} else if cell == 2 {
scratch.twos_row[row] |= 1u8 << column;
scratch.twos_column[column] |= 1u8 << row;
}
let height = scratch.heights[column];
let vertical_addition = if cell as i32 > height {
readiness(cell as i32 - height)
} else {
0.0
};
let horizontal_cost = minimum_horizontal_addition_cost(
cell as i32,
horizontal.start[column] as i32,
horizontal.end[column] as i32,
horizontal.length[column] as i32,
&scratch.heights,
elevation,
&prefix,
);
let horizontal_addition = if horizontal_cost < 0.0 {
0.0
} else {
readiness(horizontal_cost as i32)
};
let addition = union_readiness(horizontal_addition, vertical_addition);
scratch.vertical_addition[index] = vertical_addition;
scratch.horizontal_addition[index] = horizontal_addition;
scratch.addition[index] = addition;
f.direct_potential += addition;
}
}
// --- stage 3: release inventory ----------------------------------------
let mut remaining = scratch.present_bits;
while remaining != 0 {
let index = remaining.trailing_zeros() as usize;
remaining &= remaining - 1;
let row = index / BOARD_SIZE;
let column = index % BOARD_SIZE;
let horizontal = &RUN_TABLE[scratch.row_mask[row] as usize];
let vertical = &RUN_TABLE[scratch.column_mask[column] as usize];
let mut horizontal_support = [0.0f64; BOARD_SIZE];
let mut vertical_support = [0.0f64; BOARD_SIZE];
let mut horizontal_count = 0usize;
let mut vertical_count = 0usize;
let mut scan_col = horizontal.start[column] as i32;
while scan_col <= horizontal.end[column] as i32 {
let supporter = row * BOARD_SIZE + scan_col as usize;
if supporter != index && scratch.present(supporter) {
horizontal_support[horizontal_count] = scratch.addition[supporter];
horizontal_count += 1;
}
scan_col += 1;
}
let mut scan_row = vertical.start[row] as i32;
while scan_row <= vertical.end[row] as i32 {
let supporter = scan_row as usize * BOARD_SIZE + column;
if supporter != index && scratch.present(supporter) {
vertical_support[vertical_count] = scratch.addition[supporter];
vertical_count += 1;
}
scan_row += 1;
}
let value = cells[index] as i32;
let horizontal_release = release_readiness(
horizontal.length[column] as i32 - value,
&mut horizontal_support,
horizontal_count,
);
let vertical_release = release_readiness(
vertical.length[row] as i32 - value,
&mut vertical_support,
vertical_count,
);
let release = union_readiness(horizontal_release, vertical_release);
scratch.horizontal_release[index] = horizontal_release;
scratch.vertical_release[index] = vertical_release;
scratch.release[index] = release;
f.latent_chain_potential += release;
if value <= 2
&& horizontal.length[column] as i32 > value
&& vertical.length[row] as i32 > value
{
f.dead_low_numbers += 1.0 - union_readiness(scratch.addition[index], release);
}
}
// --- stage 4: adjacent ones --------------------------------------------
let mut remaining = scratch.ones_bits;
while remaining != 0 {
let index = remaining.trailing_zeros() as usize;
remaining &= remaining - 1;
let row = index / BOARD_SIZE;
let column = index % BOARD_SIZE;
if column + 1 < BOARD_SIZE && cells[index + 1] == 1 {
let escape = union_readiness(
scratch.vertical_addition[index],
scratch.vertical_release[index],
)
.max(union_readiness(
scratch.vertical_addition[index + 1],
scratch.vertical_release[index + 1],
));
f.adjacent_ones += 1.0 - escape;
}
if row + 1 < BOARD_SIZE && cells[index + BOARD_SIZE] == 1 {
let escape = union_readiness(
scratch.horizontal_addition[index],
scratch.horizontal_release[index],
)
.max(union_readiness(
scratch.horizontal_addition[index + BOARD_SIZE],
scratch.horizontal_release[index + BOARD_SIZE],
));
f.adjacent_ones += 1.0 - escape;
}
}
// --- stage 5: runs of twos ----------------------------------------------
for row in 0..BOARD_SIZE {
let mut mask = scratch.twos_row[row];
while mask != 0 {
let start = mask.trailing_zeros() as usize;
let mut run = mask >> start;
let mut length = 0usize;
while run & 1 != 0 {
length += 1;
run >>= 1;
}
let excess = length as i32 - 2;
if excess > 0 {
let mut escape = 0.0f64;
for column in start..start + length {
let slot = row * BOARD_SIZE + column;
escape = escape.max(union_readiness(
scratch.vertical_addition[slot],
scratch.vertical_release[slot],
));
}
f.triple_twos += (excess * excess) as f64 * (1.0 - escape);
}
mask &= !(((1u8 << length) - 1) << start);
}
}
for column in 0..BOARD_SIZE {
let mut mask = scratch.twos_column[column];
while mask != 0 {
let start = mask.trailing_zeros() as usize;
let mut run = mask >> start;
let mut length = 0usize;
while run & 1 != 0 {
length += 1;
run >>= 1;
}
let excess = length as i32 - 2;
if excess > 0 {
let mut escape = 0.0f64;
for row in start..start + length {
let slot = row * BOARD_SIZE + column;
escape = escape.max(union_readiness(
scratch.horizontal_addition[slot],
scratch.horizontal_release[slot],
));
}
f.triple_twos += (excess * excess) as f64 * (1.0 - escape);
}
mask &= !(((1u8 << length) - 1) << start);
}
}
// --- stage 6: cover exposure --------------------------------------------
let mut remaining = scratch.cover_bits;
while remaining != 0 {
let index = remaining.trailing_zeros() as usize;
remaining &= remaining - 1;
let row = index / BOARD_SIZE;
let column = index % BOARD_SIZE;
let mut support = [0.0f64; 4];
let mut count = 0usize;
// Neighbour order {-1,0},{1,0},{0,-1},{0,1} as in the reference.
if row > 0 && scratch.present(index - BOARD_SIZE) {
support[count] = union_readiness(
scratch.addition[index - BOARD_SIZE],
scratch.release[index - BOARD_SIZE],
);
count += 1;
}
if row + 1 < BOARD_SIZE && scratch.present(index + BOARD_SIZE) {
support[count] = union_readiness(
scratch.addition[index + BOARD_SIZE],
scratch.release[index + BOARD_SIZE],
);
count += 1;
}
if column > 0 && scratch.present(index - 1) {
support[count] =
union_readiness(scratch.addition[index - 1], scratch.release[index - 1]);
count += 1;
}
if column + 1 < BOARD_SIZE && scratch.present(index + 1) {
support[count] =
union_readiness(scratch.addition[index + 1], scratch.release[index + 1]);
count += 1;
}
sort_descending(&mut support[..count]);
if cells[index] == CRACKED {
let mut inverse = 1.0f64;
for item in support.iter().take(count) {
inverse *= 1.0 - item;
}
f.cracked_exposure += 1.0 - inverse;
} else {
f.solid_exposure += (if count > 0 { support[0] * 0.35 } else { 0.0 })
+ (if count > 1 { support[1] * 0.65 } else { 0.0 });
}
}
f
}
}