Compare commits

...

5 Commits

Author SHA1 Message Date
3ad74c90b7 elo: fix display 2025-04-24 13:06:08 -04:00
84b175d844 elo: shuffle prop_arena 2025-04-24 13:02:49 -04:00
57dba8ad1e elo: reduce k value 2025-04-24 13:00:06 -04:00
66be2185f9 add tests for MoveValueStats 2025-04-24 12:58:06 -04:00
e8d05e0f9d test 2025-04-24 11:10:11 -04:00
4 changed files with 155 additions and 111 deletions

View File

@@ -1,11 +1,11 @@
use crate::{
agent::Agent,
agent::{Agent, RandomAgent},
complexagent::ComplexAgent,
game_inner::GameInner,
logic::{ChildrenEvalMethod, FutureMoveConfig},
repr::{Board, Piece, Winner},
};
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressDrawTarget, ProgressStyle};
use indicatif::{ParallelProgressIterator, ProgressStyle};
use rand::seq::SliceRandom;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use skillratings::{
@@ -110,7 +110,7 @@ pub fn run() {
.collect()
});
let vec: Vec<(String, AgentMaker)> = configs
let mut vec: Vec<(String, AgentMaker)> = configs
.into_iter()
.map(move |config| -> (String, AgentMaker) {
(
@@ -119,10 +119,10 @@ pub fn run() {
)
})
.collect();
// vec.push((
// "RandomAgent".to_string(),
// Box::new(move |piece| Box::new(RandomAgent::new(piece))),
// ));
vec.push((
"RandomAgent".to_string(),
Box::new(move |piece| Box::new(RandomAgent::new(piece))),
));
let mut arena = PlayerArena::new(vec);
@@ -189,7 +189,6 @@ impl PlayerArena {
// Spawn parallel processing in a dedicated thread
let processing_thread = {
let sender = sender.clone();
let term = term.clone();
std::thread::spawn(move || {
rayon::ThreadPoolBuilder::new()
@@ -203,22 +202,12 @@ impl PlayerArena {
created_pairs
.into_par_iter()
.progress_with({
let a = ProgressBar::new(num as u64).with_style(
.progress_with_style({
ProgressStyle::with_template(
"[{elapsed_precise}] {pos:>7}/{len:7} ETA: {eta}",
)
.expect("invalid ProgressStyle"),
);
a.set_draw_target(ProgressDrawTarget::term(term, 5));
a
.expect("invalid ProgressStyle")
})
.progress_with_style(
ProgressStyle::with_template(
"[{elapsed_precise}] {pos:>7}/{len:7} ETA: {eta}",
)
.expect("invalid ProgressStyle"),
)
.map(|((i, j), (p1, p2))| (i, j, Self::play_two_inner(p1, p2)))
.for_each(|(i, j, o)| {
sender.send((i, j, o)).expect("Failed to send result");
@@ -253,17 +242,16 @@ impl PlayerArena {
}
fn prop_arena(&mut self, n: usize) {
self.play(
&(0..self.players.len())
let mut games = (0..self.players.len())
.flat_map(|i| {
(0..self.players.len())
.map(move |j| (i, j))
.filter(|(i, j)| i != j)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
.repeat(n),
);
.repeat(n);
games.shuffle(&mut rand::rng());
self.play(&games);
}
fn process_outcome(&mut self, player1: usize, player2: usize, outcome: &Outcomes) {
@@ -271,7 +259,7 @@ impl PlayerArena {
&self.players[player1].2,
&self.players[player2].2,
outcome,
&EloConfig::new(),
&EloConfig { k: 10.0 },
);
self.players[player1].2 = np1;
self.players[player2].2 = np2;

View File

@@ -1,5 +1,6 @@
mod board_value;
mod future_moves;
mod r#move;
mod mvs;
pub use future_moves::{ChildrenEvalMethod, FutureMoveConfig, FutureMoves};
pub use r#move::MoveCoord;

View File

@@ -1,78 +1,12 @@
use std::cmp::Ordering;
use super::board_value::BoardValueMap;
use super::{
board_value::BoardValueMap,
mvs::{MVSGameState, MoveValueStats},
};
use crate::repr::{Board, CoordPair, Piece, Winner};
use allocative::Allocative;
pub type MoveCoord = Option<CoordPair>;
#[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<MVSGameState>,
wins: u16,
losses: u16,
pub value: i32,
}
impl MoveValueStats {
fn chance_win(&self) -> Option<f32> {
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::<u16>()
+ others
.iter()
.filter(|x| x.state == Some(MVSGameState::Win))
.count() as u16;
let losses = others.iter().map(|x| x.losses).sum::<u16>()
+ 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<Ordering> {
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)
@@ -131,23 +65,21 @@ impl Move {
match m.winner {
Winner::Player(piece) => {
if piece == agent_color {
m.value.wins += 1;
m.value.state = Some(MVSGameState::Win);
m.value.set_state(Some(MVSGameState::Win));
} else {
m.value.losses += 1;
m.value.state = Some(MVSGameState::Loss);
m.value.set_state(Some(MVSGameState::Loss));
}
}
Winner::Tie => {
m.value.state = Some(MVSGameState::Tie);
m.value.set_state(Some(MVSGameState::Tie));
}
Winner::None => {}
}
if !mvc.self_value_raw {
m.self_value = m.compute_self_value(agent_color, &board, mvc);
} else {
if mvc.self_value_raw {
m.self_value = const { BoardValueMap::weighted() }.board_value(&board, agent_color);
} else {
m.self_value = m.compute_self_value(agent_color, &board, mvc);
}
m
}

123
src/logic/mvs.rs Normal file
View File

@@ -0,0 +1,123 @@
use allocative::Allocative;
use std::cmp::Ordering;
#[derive(Clone, Copy, PartialEq, Eq, Allocative, Debug, PartialOrd, Ord)]
pub enum MVSGameState {
Win = 1,
Tie = 0,
Loss = -1,
}
#[derive(Clone, Copy, Debug, Allocative, PartialEq, Eq, Default)]
pub struct MoveValueStats {
state: Option<MVSGameState>,
wins: u16,
losses: u16,
pub value: i32,
}
impl MoveValueStats {
#[cfg(test)]
pub const fn new_from_wins_losses(wins: u16, losses: u16) -> Self {
Self {
state: None,
wins,
losses,
value: 0,
}
}
#[cfg(test)]
pub const fn new_from_value(value: i32) -> Self {
Self {
state: None,
wins: 0,
losses: 0,
value,
}
}
#[cfg(test)]
pub const fn new_from_state(state: Option<MVSGameState>) -> Self {
Self {
state,
wins: 0,
losses: 0,
value: 0,
}
}
fn chance_win(&self) -> Option<f32> {
let sum = self.losses + self.wins;
if sum == 0 {
return None;
}
Some(self.wins as f32 / sum as f32)
}
pub const fn set_state(&mut self, state: Option<MVSGameState>) {
self.state = state;
}
pub fn populate_self_from_children(&mut self, others: &[Self]) {
self.wins = others.iter().map(|x| x.wins).sum::<u16>()
+ others
.iter()
.filter(|x| x.state == Some(MVSGameState::Win))
.count() as u16;
self.losses = others.iter().map(|x| x.losses).sum::<u16>()
+ others
.iter()
.filter(|x| x.state == Some(MVSGameState::Loss))
.count() as u16;
}
}
impl PartialOrd for MoveValueStats {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
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, o_cw) = (self.chance_win(), other.chance_win());
if s_cw > o_cw {
return Ordering::Greater;
} else if o_cw > s_cw {
return Ordering::Less;
}
self.value.cmp(&other.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_prob() {
let one = MoveValueStats::new_from_wins_losses(10, 4);
let two = MoveValueStats::new_from_wins_losses(4, 6);
assert!(one > two);
}
#[test]
fn one_prob_one_non() {
let one = MoveValueStats::new_from_wins_losses(10, 4);
let two = MoveValueStats::new_from_value(10);
assert!(one > two);
}
#[test]
fn one_prob_one_win() {
let one = MoveValueStats::new_from_wins_losses(10, 4);
let two = MoveValueStats::new_from_state(Some(MVSGameState::Win));
assert!(one < two);
}
}