use std::cmp::Ordering; use super::board_value::BoardValueMap; use crate::repr::{Board, CoordPair, Piece, Winner}; use allocative::Allocative; pub type MoveCoord = Option; #[derive(Clone, Copy, PartialEq, Eq, Allocative, Debug, PartialOrd, Ord)] pub enum MVSGameState { Win = 1, Loss = 0, Tie = -1, } #[derive(Clone, Copy, Debug, Allocative, PartialEq, Eq, Default)] pub struct MoveValueStats { state: Option, wins: u16, losses: u16, pub value: i32, } impl MoveValueStats { fn chance_win(&self) -> Option { let sum = self.losses + self.wins; if sum == 0 { return None; } Some(self.wins as f32 / sum as f32) } pub fn populate_self_from_children(&mut self, others: &[Self]) { let wins = others.iter().map(|x| x.wins).sum::() + others .iter() .filter(|x| x.state == Some(MVSGameState::Win)) .count() as u16; let losses = others.iter().map(|x| x.losses).sum::() + others .iter() .filter(|x| x.state == Some(MVSGameState::Loss)) .count() as u16; self.wins = wins; self.losses = losses; } } impl PartialOrd for MoveValueStats { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for MoveValueStats { fn cmp(&self, other: &Self) -> Ordering { if self.state.is_some() && other.state.is_some() { return self.state.cmp(&other.state); } let s_cw = self.chance_win(); let o_cw = other.chance_win(); if s_cw.is_some() && o_cw.is_some() { if s_cw > o_cw { return Ordering::Greater; } else if o_cw > s_cw { return Ordering::Less; } } self.value.cmp(&other.value) } } #[derive(Clone, Debug, Allocative)] pub struct Move { /// Coordinates (i, j) of the move (if it exists) pub coord: MoveCoord, /// Current winner of the match pub winner: Winner, /// Index of this move's parent pub parent: Option, /// Indices of this Move's Children // PERF! this accounts for ~40% of memory usage // I'm thinking maybe switching to this actually being `Vec>` // and ditching the entire `Arena` idea, that would have // cascading effects though, something to think about pub children: Vec, /// Value of this move (including children) pub value: MoveValueStats, /// What is the inherit value of this move (not including children) pub self_value: i16, /// Which color made a move on this move? pub color: Piece, /// Was this move's children previously trimmed? pub is_trimmed: bool, } pub struct MoveValueConfig { pub self_value_raw: bool, } impl Move { pub fn new( coord: MoveCoord, board: Board, color: Piece, agent_color: Piece, mvc: MoveValueConfig, ) -> Self { let mut m = Move { coord, winner: board.game_winner(), parent: None, children: Vec::new(), value: Default::default(), color, is_trimmed: false, self_value: 0, }; // set wins/losses values appropriately match m.winner { Winner::Player(piece) => { if piece == agent_color { m.value.wins += 1; m.value.state = Some(MVSGameState::Win); } else { m.value.losses += 1; m.value.state = Some(MVSGameState::Loss); } } Winner::Tie => { m.value.state = Some(MVSGameState::Tie); } Winner::None => {} } if !mvc.self_value_raw { m.self_value = m.compute_self_value(agent_color, &board, mvc); } else { m.self_value = const { BoardValueMap::weighted() }.board_value(&board, agent_color); } m } fn compute_self_value(&self, agent_color: Piece, board: &Board, _mvc: MoveValueConfig) -> i16 { if self.winner == Winner::Player(!agent_color) { // if this board results in the opponent winning, MAJORLY negatively weigh this move // NOTE! this branch isn't completely deleted because if so, the bot wouldn't make a move. // We shouldn't prune branches because we still need to always react to the opponent's moves return i16::MIN + 1; } else if self.winner == Winner::Player(agent_color) { // results in a win for the agent return i16::MAX - 1; } // I guess ignore Ties here, don't give them an explicit value, const { BoardValueMap::weighted() }.board_value(board, agent_color) } /// Sort children of the [`Move`] by their self_value in `arena` pub fn sort_children(&mut self, arena: &[Move]) { self.children.sort_by(|&a, &b| { arena[b].value.cmp(&arena[a].value) // Descending order for agent's max nodes }); } }