approaches/value-policy-learning/klein-friedmann-linear-q/rust/src/policy.rsRust148 lines · 4.1 KB
// Policies the evaluation binary can field on identical seeds.  Every policy
// reads a `PublicView` only.

use drop7_rs::board::BOARD_SIZE;
use drop7_rs::rng::{mix32, Mulberry32};
use drop7_rs::search::{work_bound_for, DepthTable, FairLeaf, SearchParams, Searcher};

use crate::features::FEATURE_COUNT;
use crate::learn::greedy_legal;
use crate::view::PublicView;

/// Policy-sampling RNG domain for the random arm; derived from the policy
/// seed and the game's cohort ORDINAL, never from the environment seed.
pub const RANDOM_POLICY_DOMAIN: u32 = 0x504f_4c59; // "POLY"

pub struct Decision {
    pub column: usize,
    pub work: u64,
    pub complete: bool,
}

pub trait Policy: Send {
    fn name(&self) -> &str;
    fn start_game(&mut self, _ordinal: u32) {}
    fn choose(&mut self, view: &PublicView) -> Decision;
}

/// Uniform over legal columns.
pub struct RandomLegal {
    policy_seed: u32,
    rng: Mulberry32,
}

impl RandomLegal {
    pub fn new(policy_seed: u32) -> RandomLegal {
        RandomLegal {
            policy_seed,
            rng: Mulberry32::new(0),
        }
    }
}

impl Policy for RandomLegal {
    fn name(&self) -> &str {
        "random"
    }
    fn start_game(&mut self, ordinal: u32) {
        self.rng = Mulberry32::new(mix32(
            self.policy_seed ^ ordinal.wrapping_add(1).wrapping_mul(0x9e37_79b9) ^ RANDOM_POLICY_DOMAIN,
        ));
    }
    fn choose(&mut self, view: &PublicView) -> Decision {
        let legal: Vec<usize> = (0..BOARD_SIZE).filter(|&c| view.is_legal(c)).collect();
        let pick = ((self.rng.next_bits() as u64 * legal.len() as u64) >> 32) as usize;
        Decision {
            column: legal[pick],
            work: 0,
            complete: true,
        }
    }
}

/// The engine's own fallback order: 3, 2, 4, 1, 5, 0, 6.
pub struct CenterFirst;

impl Policy for CenterFirst {
    fn name(&self) -> &str {
        "center"
    }
    fn choose(&mut self, view: &PublicView) -> Decision {
        const ORDER: [usize; 7] = [3, 2, 4, 1, 5, 0, 6];
        let column = ORDER.into_iter().find(|&c| view.is_legal(c)).expect("legal column");
        Decision {
            column,
            work: 0,
            complete: true,
        }
    }
}

/// The six-feature linear Q policy with frozen weights.
pub struct LinearQ {
    name: String,
    pub weights: [f64; FEATURE_COUNT],
}

impl LinearQ {
    pub fn new(name: &str, weights: [f64; FEATURE_COUNT]) -> LinearQ {
        LinearQ {
            name: name.to_string(),
            weights,
        }
    }
}

impl Policy for LinearQ {
    fn name(&self) -> &str {
        &self.name
    }
    fn choose(&mut self, view: &PublicView) -> Decision {
        Decision {
            column: greedy_legal(&self.weights, view),
            work: 7,
            complete: true,
        }
    }
}

/// Fair expectimax from drop7-rs with the frozen fair leaf: the program's
/// reference family.  Terminal utility and policy seed match the repository's
/// deployment parameters (approaches/lifetime-objective/nnue-evolution).
pub struct FairSearch {
    name: String,
    depth: i32,
    searcher: Searcher<FairLeaf, DepthTable>,
}

impl FairSearch {
    pub fn new(depth: i32, strata: i32, table_entries: usize) -> FairSearch {
        let params = SearchParams {
            depth,
            chance_samples: strata,
            terminal_utility: -1_000_000.0,
            maximum_work: work_bound_for(depth, strata) + 1,
            policy_seed: 0xd707_5eed,
        };
        FairSearch {
            name: format!("fair-d{depth}s{strata}"),
            depth,
            searcher: Searcher::new(params, FairLeaf::default(), DepthTable::new(table_entries, 1)),
        }
    }
}

impl Policy for FairSearch {
    fn name(&self) -> &str {
        &self.name
    }
    fn choose(&mut self, view: &PublicView) -> Decision {
        let state = view.as_search_state();
        let (action, metrics) = self.searcher.choose_action(&state);
        Decision {
            column: if action < 0 { BOARD_SIZE } else { action as usize },
            work: metrics.work,
            complete: metrics.completed_depth >= self.depth,
        }
    }
}