From ae02672e2af3b586c5b73d337b2a0f33153b45ea Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Fri, 15 Mar 2024 17:12:56 +0100 Subject: [PATCH 01/27] start of console game version --- src/console_game.rs | 33 +++++++++++++++++++++++++++++++++ src/main.rs | 7 ++++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/console_game.rs diff --git a/src/console_game.rs b/src/console_game.rs new file mode 100644 index 0000000..9ebf2c4 --- /dev/null +++ b/src/console_game.rs @@ -0,0 +1,33 @@ +use crate::mcts::{MCTS, Node}; +use crate::othello::{State, Action, print_state}; + +pub fn console_game(){ + let mut state = State::new(); + let mut mcts = MCTS::new(Node::new(state, None, state.get_actions())); + let mut choice: Result; + print_state(state); + let mut buf = String::new(); + let mut pos: (usize, usize) = (0, 0); + loop { + loop { + print!("Enter coordinates for desired move: "); + let _ = std::io::stdin().read_line(&mut buf); + let cmd: Vec<&str> = buf.trim().split(",").clone().collect(); + match (cmd.get(0), cmd.get(1)) { + (Some(cmd_1), Some(cmd_2)) => { + match (cmd_1.parse::(), cmd_2.parse::()) { + (Ok(x_index), Ok(y_index)) => { + pos.0 = x_index; + pos.1 = y_index; + break; + }, + _ => println!("Please provide only numbers for indexes"), + } + }, + _ => println!("Please provide a move in the form of 1,2 "), + + } + } + + } +} diff --git a/src/main.rs b/src/main.rs index 3ddb29f..1139f43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ use ureq::Response; -use std::thread::current; use std::usize; use std::{thread::sleep, borrow::Borrow}; use std::time::Duration; mod mcts; mod othello; +mod console_game; +use console_game::console_game; use mcts::{MCTS, Node}; use othello::{State, Action, parse_state}; @@ -27,6 +28,10 @@ fn main() { x if x == "1" => ai_color = "true".to_string(), x if x == "w" => ai_color = "true".to_string(), x if x == "white" => ai_color = "true".to_string(), + x if x == "console" => { + console_game(); + return; + }, _ => panic!("Please pass a proper argument to the AI"), } From 1d27d8e601517dc225fe805796d25c7c563cd37b Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Fri, 15 Mar 2024 23:18:37 +0100 Subject: [PATCH 02/27] player vs ai can now be played in terminal --- src/console_game.rs | 43 ++++++++++++++++++++++++++++++++++--------- src/othello.rs | 11 ++++++++--- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/console_game.rs b/src/console_game.rs index 9ebf2c4..07bd72b 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -1,33 +1,58 @@ +use std::io::Write; + use crate::mcts::{MCTS, Node}; use crate::othello::{State, Action, print_state}; pub fn console_game(){ + let dev_null = |_a: usize, _b: usize| -> (){}; let mut state = State::new(); let mut mcts = MCTS::new(Node::new(state, None, state.get_actions())); - let mut choice: Result; + let mut player_choice: Option; print_state(state); let mut buf = String::new(); - let mut pos: (usize, usize) = (0, 0); - loop { + while state.remaining_moves > 0 { loop { print!("Enter coordinates for desired move: "); + let _ = std::io::stdout().flush(); let _ = std::io::stdin().read_line(&mut buf); let cmd: Vec<&str> = buf.trim().split(",").clone().collect(); match (cmd.get(0), cmd.get(1)) { (Some(cmd_1), Some(cmd_2)) => { match (cmd_1.parse::(), cmd_2.parse::()) { - (Ok(x_index), Ok(y_index)) => { - pos.0 = x_index; - pos.1 = y_index; - break; + (Ok(y_index), Ok(x_index)) => { + player_choice = Some(Action { + color: 'B', + x: x_index, + y: y_index, + }); + if state.get_actions().contains(&player_choice.clone().unwrap()) { + break; + }else { + println!("Invalid move"); + } }, _ => println!("Please provide only numbers for indexes"), } }, - _ => println!("Please provide a move in the form of 1,2 "), + (Some(skip), None) => { + if skip.to_lowercase() == "skip" { + player_choice = None; + break; + } + }, + _ => println!("Please provide a move in the form of 1,2 or \"skip\""), } + buf.clear(); } - + buf.clear(); + state = state.do_action(player_choice); + print_state(state); + if let Ok(action) = mcts.search(state, 10000, dev_null){ + state = state.do_action(Some(action)); + } else { + state = state.do_action(None); + } + print_state(state); } } diff --git a/src/othello.rs b/src/othello.rs index 1bcfe2a..8e0b580 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -199,8 +199,13 @@ pub fn parse_state(json: serde_json::Value) -> State { } pub fn print_state(state: State) { - for i in state.board { - println!("{:?}", i); + println!(" 0 1 2 3 4 5 6 7"); + for (i, row) in state.board.iter().enumerate() { + print!("{i} "); + for ch in row { + print!("|{}", ch); + } + print!("|\n"); } - println!("next: {}", state.next_turn) + println!("Next: {}", state.next_turn) } From ab6b720d2354494d8f4c18f5fe09e334a919c0c2 Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Sun, 17 Mar 2024 12:09:22 +0100 Subject: [PATCH 03/27] now plays ai vs ai --- src/console_game.rs | 71 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/src/console_game.rs b/src/console_game.rs index 07bd72b..1c260ca 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -4,13 +4,61 @@ use crate::mcts::{MCTS, Node}; use crate::othello::{State, Action, print_state}; pub fn console_game(){ - let dev_null = |_a: usize, _b: usize| -> (){}; let mut state = State::new(); - let mut mcts = MCTS::new(Node::new(state, None, state.get_actions())); + let mut mcts = MCTS::new("false",Node::new(state, None, state.get_actions())); + let mut mcts2 = MCTS::new("true",Node::new(state, None, state.get_actions())); + println!("Playing AI vs AI\n"); + loop { + state = ai_turn(&mut mcts, state.clone(), 100); + if state.remaining_moves < 0 { + break; + } + state = ai_turn(&mut mcts2, state.clone(), 1000); + + if state.remaining_moves < 0 { + break; + } + } + //print_state(state); + determine_winner(state); + println!("\nGAME OVER\n"); +} + +fn determine_winner(state: State) { + let p1 = 'B'; + let p2 = 'W'; + let mut p1_score: isize = 0; + let mut p2_score: isize = 0; + for row in state.board { + for ch in row { + if ch == p1 { + p1_score += 1; + }else if ch == p2 { + p2_score += 1; + } + } + } + println!("Score is\t{} {} : {} {}", p1, p1_score, p2_score, p2); + +} + + +fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { + let dev_null = |_a: usize, _b: usize| -> (){}; + let action = mcts.search(state.clone(), iterations, dev_null); + if action.is_ok() { + state.clone().do_action(Some(action.unwrap().clone())) + } + else { + state.clone().do_action(None) + } +} + + + +fn player_turn(state: State) -> State { let mut player_choice: Option; - print_state(state); let mut buf = String::new(); - while state.remaining_moves > 0 { loop { print!("Enter coordinates for desired move: "); let _ = std::io::stdout().flush(); @@ -35,7 +83,7 @@ pub fn console_game(){ } }, (Some(skip), None) => { - if skip.to_lowercase() == "skip" { + if skip.to_lowercase() == "skip".to_string() { player_choice = None; break; } @@ -46,13 +94,6 @@ pub fn console_game(){ buf.clear(); } buf.clear(); - state = state.do_action(player_choice); - print_state(state); - if let Ok(action) = mcts.search(state, 10000, dev_null){ - state = state.do_action(Some(action)); - } else { - state = state.do_action(None); - } - print_state(state); - } -} + state.clone().do_action(player_choice) + +} From c068f089a6cbed8dc4c662d192122fcfdfc41b0c Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Sun, 17 Mar 2024 12:10:16 +0100 Subject: [PATCH 04/27] exits after console game is done --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 8368fca..eab81f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use ureq::Response; +use std::process::exit; use std::usize; use std::{thread::sleep, borrow::Borrow}; use std::time::Duration; @@ -30,7 +31,7 @@ fn main() { x if x == "white" => ai_color = "true".to_string(), x if x == "console" => { console_game(); - return; + exit(0); }, _ => panic!("Please pass a proper argument to the AI"), From 5c910bbd5a90ae8c7b51dd0d3b44d32e411f25dc Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Sun, 17 Mar 2024 13:17:25 +0100 Subject: [PATCH 05/27] now allows testing different ai setups against eachother --- src/console_game.rs | 132 ++++++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 60 deletions(-) diff --git a/src/console_game.rs b/src/console_game.rs index 1c260ca..ec8d508 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -1,30 +1,37 @@ use std::io::Write; +use std::isize; -use crate::mcts::{MCTS, Node}; -use crate::othello::{State, Action, print_state}; +use crate::mcts::MCTS; +use crate::othello::{State, Action}; pub fn console_game(){ - let mut state = State::new(); - let mut mcts = MCTS::new("false",Node::new(state, None, state.get_actions())); - let mut mcts2 = MCTS::new("true",Node::new(state, None, state.get_actions())); - println!("Playing AI vs AI\n"); - loop { - state = ai_turn(&mut mcts, state.clone(), 100); - if state.remaining_moves < 0 { - break; - } - state = ai_turn(&mut mcts2, state.clone(), 1000); + let mut win_balance: isize = 0; + println!("Playing AI e1.0 vs AI e2.0 \n"); + for i in 1..11 { + print!("{i}... "); + let mut state = State::new(); + let mut mcts = MCTS::new("false",1.0); + let mut mcts2 = MCTS::new("true",2.0); + _ = std::io::stdout().flush(); + loop { + state = ai_turn(&mut mcts, state.clone(), 500); + if state.remaining_moves < 0 { + break; + } + state = ai_turn(&mut mcts2, state.clone(), 500); - if state.remaining_moves < 0 { - break; + if state.remaining_moves < 0 { + break; + } } + //print_state(state); + win_balance += determine_winner(state); + //println!("\nGAME OVER\n"); } - //print_state(state); - determine_winner(state); - println!("\nGAME OVER\n"); + println!("\nResult: {win_balance}") } -fn determine_winner(state: State) { +fn determine_winner(state: State) -> isize { let p1 = 'B'; let p2 = 'W'; let mut p1_score: isize = 0; @@ -38,20 +45,25 @@ fn determine_winner(state: State) { } } } - println!("Score is\t{} {} : {} {}", p1, p1_score, p2_score, p2); + match p1_score - p2_score { + x if x > 0 => 1, + x if x < 0 => -1, + _ => 0, + } + //println!("Score is\t{} {} : {} {}", p1, p1_score, p2_score, p2); } fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |_a: usize, _b: usize| -> (){}; - let action = mcts.search(state.clone(), iterations, dev_null); - if action.is_ok() { - state.clone().do_action(Some(action.unwrap().clone())) - } - else { - state.clone().do_action(None) - } + let dev_null = |_a: usize, _b: usize| -> (){}; + let action = mcts.search(state.clone(), iterations, dev_null); + if action.is_ok() { + state.clone().do_action(Some(action.unwrap().clone())) + } + else { + state.clone().do_action(None) + } } @@ -59,41 +71,41 @@ fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { fn player_turn(state: State) -> State { let mut player_choice: Option; let mut buf = String::new(); - loop { - print!("Enter coordinates for desired move: "); - let _ = std::io::stdout().flush(); - let _ = std::io::stdin().read_line(&mut buf); - let cmd: Vec<&str> = buf.trim().split(",").clone().collect(); - match (cmd.get(0), cmd.get(1)) { - (Some(cmd_1), Some(cmd_2)) => { - match (cmd_1.parse::(), cmd_2.parse::()) { - (Ok(y_index), Ok(x_index)) => { - player_choice = Some(Action { - color: 'B', - x: x_index, - y: y_index, - }); - if state.get_actions().contains(&player_choice.clone().unwrap()) { - break; - }else { - println!("Invalid move"); - } - }, - _ => println!("Please provide only numbers for indexes"), - } - }, - (Some(skip), None) => { - if skip.to_lowercase() == "skip".to_string() { - player_choice = None; - break; - } - }, - _ => println!("Please provide a move in the form of 1,2 or \"skip\""), + loop { + print!("Enter coordinates for desired move: "); + let _ = std::io::stdout().flush(); + let _ = std::io::stdin().read_line(&mut buf); + let cmd: Vec<&str> = buf.trim().split(",").clone().collect(); + match (cmd.get(0), cmd.get(1)) { + (Some(cmd_1), Some(cmd_2)) => { + match (cmd_1.parse::(), cmd_2.parse::()) { + (Ok(y_index), Ok(x_index)) => { + player_choice = Some(Action { + color: 'B', + x: x_index, + y: y_index, + }); + if state.get_actions().contains(&player_choice.clone().unwrap()) { + break; + }else { + println!("Invalid move"); + } + }, + _ => println!("Please provide only numbers for indexes"), + } + }, + (Some(skip), None) => { + if skip.to_lowercase() == "skip".to_string() { + player_choice = None; + break; + } + }, + _ => println!("Please provide a move in the form of 1,2 or \"skip\""), - } - buf.clear(); } buf.clear(); - state.clone().do_action(player_choice) + } + buf.clear(); + state.clone().do_action(player_choice) } From 9f647a0e3290eba160740c947d9fd1b76d3e3385 Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Sun, 17 Mar 2024 18:23:53 +0100 Subject: [PATCH 06/27] created testing binary for faster ai vs ai simulation --- src/console_game.rs | 46 ++++++++++++++++++++++----------------------- src/main.rs | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/console_game.rs b/src/console_game.rs index ec8d508..b130c40 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -2,38 +2,37 @@ use std::io::Write; use std::isize; use crate::mcts::MCTS; -use crate::othello::{State, Action}; +use crate::othello::{State, Action, print_state}; pub fn console_game(){ let mut win_balance: isize = 0; - println!("Playing AI e1.0 vs AI e2.0 \n"); - for i in 1..11 { - print!("{i}... "); - let mut state = State::new(); - let mut mcts = MCTS::new("false",1.0); - let mut mcts2 = MCTS::new("true",2.0); - _ = std::io::stdout().flush(); - loop { - state = ai_turn(&mut mcts, state.clone(), 500); - if state.remaining_moves < 0 { - break; - } - state = ai_turn(&mut mcts2, state.clone(), 500); + let a = 1.5; + println!("Game mode: player vs AI\n"); + let mut state = State::new(); + let mut mcts = MCTS::new("true", a); + _ = std::io::stdout().flush(); + loop { + print_state(state); + state = player_turn(state.clone()); + if state.remaining_moves < 0 { + break; + } + print_state(state); + state = ai_turn(&mut mcts, state.clone(), 500); - if state.remaining_moves < 0 { - break; - } + if state.remaining_moves < 0 { + break; } - //print_state(state); - win_balance += determine_winner(state); - //println!("\nGAME OVER\n"); } + //print_state(state); + win_balance += determine_winner(state); + //println!("\nGAME OVER\n"); println!("\nResult: {win_balance}") } fn determine_winner(state: State) -> isize { - let p1 = 'B'; - let p2 = 'W'; + let p1 = 'W'; + let p2 = 'B'; let mut p1_score: isize = 0; let mut p2_score: isize = 0; for row in state.board { @@ -108,4 +107,5 @@ fn player_turn(state: State) -> State { buf.clear(); state.clone().do_action(player_choice) -} +} + diff --git a/src/main.rs b/src/main.rs index 09f745f..e845c28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ mod mcts; mod othello; mod console_game; use console_game::console_game; -use mcts::{MCTS, Node}; +use mcts::MCTS; use othello::{State, Action, parse_state}; From c9abf19cf3b50a27bfe83a3e1d0a112d42755747 Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Sun, 17 Mar 2024 18:24:56 +0100 Subject: [PATCH 07/27] added new files --- src/bin/ai-test.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ 2 files changed, 65 insertions(+) create mode 100644 src/bin/ai-test.rs create mode 100644 src/lib.rs diff --git a/src/bin/ai-test.rs b/src/bin/ai-test.rs new file mode 100644 index 0000000..65b7d5a --- /dev/null +++ b/src/bin/ai-test.rs @@ -0,0 +1,63 @@ +use std::isize; +use rusty_othello_ai::mcts::MCTS; +use rusty_othello_ai::othello::State; + +pub fn main(){ + let args: Vec = std::env::args().collect(); + let mut win_balance: isize = 0; + let a: f64 = args.get(1).expect("Missing value for A").parse().expect("Not a valid floatingpoint number"); + let b: f64 = args.get(2).expect("Missing value for A").parse().expect("Not a valid floatingpoint number"); + + let mut state = State::new(); + let mut mcts = MCTS::new("true", a); + let mut mcts2 = MCTS::new("false", b); + loop { + state = ai_turn(&mut mcts, state.clone(), 500); + if state.remaining_moves < 0 { + break; + } + state = ai_turn(&mut mcts2, state.clone(), 500); + + if state.remaining_moves < 0 { + break; + } + } + win_balance += determine_winner(state); + println!("{win_balance}") +} + +fn determine_winner(state: State) -> isize { + let p1 = 'W'; + let p2 = 'B'; + let mut p1_score: isize = 0; + let mut p2_score: isize = 0; + for row in state.board { + for ch in row { + if ch == p1 { + p1_score += 1; + }else if ch == p2 { + p2_score += 1; + } + } + } + match p1_score - p2_score { + x if x > 0 => 1, + x if x < 0 => -1, + _ => 0, + } + //println!("Score is\t{} {} : {} {}", p1, p1_score, p2_score, p2); + +} + + +fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { + let dev_null = |_a: usize, _b: usize| -> (){}; + let action = mcts.search(state.clone(), iterations, dev_null); + if action.is_ok() { + state.clone().do_action(Some(action.unwrap().clone())) + } + else { + state.clone().do_action(None) + } +} + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bd21b18 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod mcts; +pub mod othello; From b7bd16fb026df69ff5066ba3760b7e7e8b38ea40 Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Mon, 18 Mar 2024 18:31:34 +0100 Subject: [PATCH 08/27] updated to match memory reduction changes --- src/bin/ai-test.rs | 15 ++++++++------- src/console_game.rs | 12 +++++++----- src/othello.rs | 9 +++++++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/bin/ai-test.rs b/src/bin/ai-test.rs index 65b7d5a..e830860 100644 --- a/src/bin/ai-test.rs +++ b/src/bin/ai-test.rs @@ -5,30 +5,31 @@ use rusty_othello_ai::othello::State; pub fn main(){ let args: Vec = std::env::args().collect(); let mut win_balance: isize = 0; - let a: f64 = args.get(1).expect("Missing value for A").parse().expect("Not a valid floatingpoint number"); - let b: f64 = args.get(2).expect("Missing value for A").parse().expect("Not a valid floatingpoint number"); + let a: f32 = args.get(1).expect("Missing value for A").parse().expect("Not a valid floatingpoint number"); + let b: f32 = args.get(2).expect("Missing value for A").parse().expect("Not a valid floatingpoint number"); let mut state = State::new(); let mut mcts = MCTS::new("true", a); let mut mcts2 = MCTS::new("false", b); + let mut ai_iterations = 500; loop { - state = ai_turn(&mut mcts, state.clone(), 500); + state = ai_turn(&mut mcts, state.clone(), ai_iterations); if state.remaining_moves < 0 { break; } - state = ai_turn(&mut mcts2, state.clone(), 500); - + state = ai_turn(&mut mcts2, state.clone(), ai_iterations); if state.remaining_moves < 0 { break; } + ai_iterations += ai_iterations / 100; } win_balance += determine_winner(state); println!("{win_balance}") } fn determine_winner(state: State) -> isize { - let p1 = 'W'; - let p2 = 'B'; + let p1 = 1; + let p2 = 0; let mut p1_score: isize = 0; let mut p2_score: isize = 0; for row in state.board { diff --git a/src/console_game.rs b/src/console_game.rs index b130c40..bec832a 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -6,11 +6,12 @@ use crate::othello::{State, Action, print_state}; pub fn console_game(){ let mut win_balance: isize = 0; - let a = 1.5; + let a = 1.0; println!("Game mode: player vs AI\n"); let mut state = State::new(); let mut mcts = MCTS::new("true", a); _ = std::io::stdout().flush(); + let mut ai_iterations = 5000; loop { print_state(state); state = player_turn(state.clone()); @@ -18,7 +19,8 @@ pub fn console_game(){ break; } print_state(state); - state = ai_turn(&mut mcts, state.clone(), 500); + state = ai_turn(&mut mcts, state.clone(), ai_iterations); + ai_iterations += ai_iterations / 100; if state.remaining_moves < 0 { break; @@ -31,8 +33,8 @@ pub fn console_game(){ } fn determine_winner(state: State) -> isize { - let p1 = 'W'; - let p2 = 'B'; + let p1 = 1; + let p2 = 0; let mut p1_score: isize = 0; let mut p2_score: isize = 0; for row in state.board { @@ -80,7 +82,7 @@ fn player_turn(state: State) -> State { match (cmd_1.parse::(), cmd_2.parse::()) { (Ok(y_index), Ok(x_index)) => { player_choice = Some(Action { - color: 'B', + color: 1, x: x_index, y: y_index, }); diff --git a/src/othello.rs b/src/othello.rs index e9de461..e624da4 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -207,9 +207,14 @@ pub fn print_state(state: State) { for (i, row) in state.board.iter().enumerate() { print!("{i} "); for ch in row { - print!("|{}", ch); + let c = match ch { + 1 => 'X', + 0 => '0', + _ => '_', + }; + print!("|{}", c); } print!("|\n"); } - println!("Next: {}", state.next_turn) + println!("Next: {:2}", state.next_turn) } From 239fae10ef442c84340640798f2f5ff464683d50 Mon Sep 17 00:00:00 2001 From: Philip Cramer Date: Wed, 20 Mar 2024 15:22:17 +0100 Subject: [PATCH 09/27] updated to match changes from master branch --- src/bin/ai-test.rs | 2 +- src/console_game.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/ai-test.rs b/src/bin/ai-test.rs index e830860..a9de86e 100644 --- a/src/bin/ai-test.rs +++ b/src/bin/ai-test.rs @@ -52,7 +52,7 @@ fn determine_winner(state: State) -> isize { fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |_a: usize, _b: usize| -> (){}; + let dev_null = |_a: usize, _b: usize, _c: &i8 | -> (){}; let action = mcts.search(state.clone(), iterations, dev_null); if action.is_ok() { state.clone().do_action(Some(action.unwrap().clone())) diff --git a/src/console_game.rs b/src/console_game.rs index bec832a..874f77c 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -57,7 +57,7 @@ fn determine_winner(state: State) -> isize { fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |_a: usize, _b: usize| -> (){}; + let dev_null = |_a: usize, _b: usize, _c: &i8 | -> (){}; let action = mcts.search(state.clone(), iterations, dev_null); if action.is_ok() { state.clone().do_action(Some(action.unwrap().clone())) From 45bfe94d996043216b4d08c0c31ddcbecccfc770 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:03:23 +0100 Subject: [PATCH 10/27] initial bitwise othello Still needs proper logic. --- src/bin/ai-test.rs | 10 +-- src/console_game.rs | 12 +-- src/lib.rs | 1 + src/main.rs | 10 +-- src/mcts.rs | 22 ++--- src/othello.rs | 214 ++++++++++++++++++++++---------------------- 6 files changed, 133 insertions(+), 136 deletions(-) diff --git a/src/bin/ai-test.rs b/src/bin/ai-test.rs index a9de86e..372b919 100644 --- a/src/bin/ai-test.rs +++ b/src/bin/ai-test.rs @@ -1,6 +1,6 @@ use std::isize; use rusty_othello_ai::mcts::MCTS; -use rusty_othello_ai::othello::State; +use rusty_othello_ai::othello::{State,Color}; pub fn main(){ let args: Vec = std::env::args().collect(); @@ -23,10 +23,10 @@ pub fn main(){ } ai_iterations += ai_iterations / 100; } - win_balance += determine_winner(state); + win_balance += 1;//determine_winner(state); println!("{win_balance}") } - +/* fn determine_winner(state: State) -> isize { let p1 = 1; let p2 = 0; @@ -49,10 +49,10 @@ fn determine_winner(state: State) -> isize { //println!("Score is\t{} {} : {} {}", p1, p1_score, p2_score, p2); } - +*/ fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |_a: usize, _b: usize, _c: &i8 | -> (){}; + let dev_null = |_a: usize, _b: usize, _c: &Color | -> (){}; let action = mcts.search(state.clone(), iterations, dev_null); if action.is_ok() { state.clone().do_action(Some(action.unwrap().clone())) diff --git a/src/console_game.rs b/src/console_game.rs index 874f77c..62f7095 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -2,7 +2,7 @@ use std::io::Write; use std::isize; use crate::mcts::MCTS; -use crate::othello::{State, Action, print_state}; +use crate::othello::{State, Action, Color, print_state}; pub fn console_game(){ let mut win_balance: isize = 0; @@ -27,11 +27,11 @@ pub fn console_game(){ } } //print_state(state); - win_balance += determine_winner(state); + win_balance += 1;//determine_winner(state); //println!("\nGAME OVER\n"); println!("\nResult: {win_balance}") } - +/* fn determine_winner(state: State) -> isize { let p1 = 1; let p2 = 0; @@ -54,10 +54,10 @@ fn determine_winner(state: State) -> isize { //println!("Score is\t{} {} : {} {}", p1, p1_score, p2_score, p2); } - +*/ fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |_a: usize, _b: usize, _c: &i8 | -> (){}; + let dev_null = |_a: usize, _b: usize, _c: &Color | -> (){}; let action = mcts.search(state.clone(), iterations, dev_null); if action.is_ok() { state.clone().do_action(Some(action.unwrap().clone())) @@ -82,7 +82,7 @@ fn player_turn(state: State) -> State { match (cmd_1.parse::(), cmd_2.parse::()) { (Ok(y_index), Ok(x_index)) => { player_choice = Some(Action { - color: 1, + color: Color::WHITE, x: x_index, y: y_index, }); diff --git a/src/lib.rs b/src/lib.rs index bd21b18..677bbd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ pub mod mcts; pub mod othello; + diff --git a/src/main.rs b/src/main.rs index cccb366..795ff28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ mod othello; mod console_game; use console_game::console_game; use mcts::MCTS; -use othello::{State, Action, parse_state}; +use othello::{State, Action, Color, parse_state}; @@ -68,7 +68,7 @@ fn main() { }, // If it's not the AI's turn, it performs a search using MCTS and waits Ok(false) => { - let dev_null = |_a: usize, _b: usize, _c: &i8| -> (){}; + let dev_null = |_a: usize, _b: usize, _c: &Color| -> (){}; _ = mcts.search(state, 1000, dev_null); //sleep(Duration::from_secs(1)); }, @@ -154,10 +154,10 @@ fn send_move(player: &String, ai_move: Option) -> Result "false", - _ => "true", + Color::BLACK => "false", + Color::WHITE => "true", }; let url = format!("{}/AIStatus/{}/{}/{}", SERVER_URL, current, total, color); _ = ureq::post(&url).call(); diff --git a/src/mcts.rs b/src/mcts.rs index ab62006..3390481 100644 --- a/src/mcts.rs +++ b/src/mcts.rs @@ -1,5 +1,5 @@ -use crate::othello::{State, Action, simulate_game}; +use crate::othello::{State, Action, Color, simulate_game}; use std::collections::HashMap; @@ -23,7 +23,7 @@ impl Node { } } - pub fn update_node(&mut self, result: (i8, isize)) { + pub fn update_node(&mut self, result: (Color, isize)) { self.visits += 1; if result.0 == self.state.next_turn { self.score += result.1; @@ -41,7 +41,7 @@ impl Node { #[derive()] pub struct MCTS { pub size: usize, - color: i8, + color: Color, expl: f32, nodes: Vec, tree: Vec>, @@ -51,10 +51,10 @@ pub struct MCTS { impl MCTS { pub fn new(col: &str, explore: f32) -> Self { - let ai_color: i8; + let ai_color: Color; match col { - b if b == "false".to_string() => ai_color = 0, - _ => ai_color = 1, + b if b == "false".to_string() => ai_color = Color::BLACK, + _ => ai_color = Color::WHITE, }; //let mut map = HashMap::new(); //map.insert(node.state, 0 as usize); @@ -71,7 +71,7 @@ impl MCTS { // Performs a Monte Carlo Tree Search from the given state for the given number of iterations // It returns the best action found or an error if no action was found - pub fn search(&mut self, from: State, iterations: usize, send_status: fn(usize, usize, &i8)) -> Result { + pub fn search(&mut self, from: State, iterations: usize, send_status: fn(usize, usize, &Color)) -> Result { if let Some(root) = self.state_map.get(&from).cloned() { for i in 0..iterations { if i % 1000 == 0 { @@ -81,7 +81,7 @@ impl MCTS { let node_index = self.select(root.clone()).clone(); let node_index = self.expand(node_index.clone()).clone(); for index in self.tree.get(node_index).expect("No child nodes to simulate").clone().iter() { - let result: (i8, isize) = self.simulate(*index); + let result: (Color, isize) = self.simulate(*index); self.backpropagate(*index, result.clone()); } @@ -155,7 +155,7 @@ impl MCTS { } // Simulates a game from the given node and returns the result - fn simulate(&mut self, node_index: usize) -> (i8, isize) { + fn simulate(&mut self, node_index: usize) -> (Color, isize) { if let Some(node) = self.nodes.get_mut(node_index) { let mut node_state = node.state.clone(); let mut score = simulate_game(&mut node_state); @@ -165,11 +165,11 @@ impl MCTS { node.update_node((node.state.next_turn, score)); return (node_state.next_turn, score); } - (-1, 0) + panic!("Node not found"); } // Updates the nodes in the MCTS from the given child node to the root based on the result of a simulated game - fn backpropagate(&mut self, child_index: usize, result: (i8, isize)) { + fn backpropagate(&mut self, child_index: usize, result: (Color, isize)) { let mut current_node: &mut Node; let mut parent_index: Option = self.parents.get(child_index).unwrap().clone(); while parent_index.is_some() { diff --git a/src/othello.rs b/src/othello.rs index e624da4..7523496 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -1,123 +1,95 @@ -use std::{isize, i16}; - +use std::{i16, isize, u16, usize}; use rand::Rng; - const BOARD_SIZE: usize = 8; - +const FIELD_SIZE: usize = 2; +const BLACK_BITMASK: u16 = 0b1010101010101010; +const WHITE_BITMASK: u16 = 0b0101010101010101; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq,)] +pub enum Color { + BLACK, + WHITE +} #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct State { - pub board: [[i8; BOARD_SIZE]; BOARD_SIZE], - pub next_turn: i8, + pub board: [u16; BOARD_SIZE], + pub next_turn: Color, pub remaining_moves: i16, } impl State { pub fn new() -> Self{ let mut new = Self { - board: [ - [-1; BOARD_SIZE]; BOARD_SIZE], - next_turn: 1, + board: [0; BOARD_SIZE], + next_turn: Color::BLACK, remaining_moves: 60, }; - new.board[3][3] = 0; - new.board[3][4] = 1; - new.board[4][4] = 0; - new.board[4][3] = 1; + new.board[3] = 0b11 << 7; + new.board[4] = 0b1001 << 6; new - } + pub fn get_actions(&self) -> Vec { let mut actions: Vec = Vec::new(); - let mut tmp_action = Action::new(self.next_turn, 0, 0); - for (x, row) in self.board.iter().enumerate(){ - for (y, ch) in row.iter().enumerate(){ - tmp_action.x = x; - tmp_action.y = y; - if *ch == -1 { - for dir in vec![(0,1), (1,0), (1,1), (0,-1), (-1,0), (-1,-1), (1,-1), (-1,1)] { - let mut tmp_state = self.clone(); - if tmp_state.flip_pieces(tmp_action.clone(), dir.0, dir.1){ - actions.push(tmp_action.clone()); - break - } - } - } - } + let mut _tmp_action = Action::new(self.next_turn, 0, 0); + for _row in self.board.iter(){ + // TODO: Fix this } - - + actions.push(_tmp_action); return actions; } - pub fn do_action(&mut self, action: Option) -> State { + pub fn do_action(&self, action: Option) -> State { let next_turn = match self.next_turn { - 0 => 1, - 1 => 0, - _ => -1, + Color::BLACK => Color::WHITE, + Color::WHITE => Color::BLACK, }; let mut new_state = State { next_turn: next_turn.clone(), board: self.board.clone(), - remaining_moves: (self.remaining_moves.clone() - 1), + remaining_moves: (self.remaining_moves.clone()), }; if action.is_some() { + new_state.remaining_moves -= 1; let act = action.unwrap(); - new_state.board[act.x][act.y] = act.color.clone(); - for dir in vec![(0,1), (1,0), (1,1), (0,-1), (-1,0), (-1,-1), (1,-1), (-1,1)] { - new_state.flip_pieces(act.clone(), dir.0, dir.1); - } + new_state.flip_pieces(act); } return new_state; } - fn flip_pieces(&mut self, action: Action, x1: isize, y1: isize) -> bool { - let mut to_flip = Vec::new(); - let mut x_index = (action.x as isize + x1) as usize; - let mut y_index = (action.y as isize + y1) as usize; - let own_color = action.color.clone(); - let opponent = match action.color { - 0 => 1, - _ => 0, - }; - loop{ - //Bounds Check - if x_index > BOARD_SIZE - 1 || y_index > BOARD_SIZE - 1 { - return false; - } - match self.board[x_index][y_index] { - x if x == own_color => break, - k if k == opponent => { - to_flip.push((x_index.clone(), y_index.clone())); - x_index = (x_index as isize + x1) as usize; - y_index = (y_index as isize + y1) as usize; - }, - _ => return false, - } - } - if to_flip.len() == 0 { - return false; - } - else { - for (x,y) in to_flip.iter() { - self.board[x.clone()][y.clone()] = action.color; - } - true - } + fn flip_pieces(&mut self, action: Action) -> bool { + let mut result = true; + result = result && self.flip_row(action.clone()); + result = result && self.flip_column(action.clone()); + result = result && self.flip_diagonals(action); + return result; + } + fn flip_row(&mut self, action: Action) -> bool { + //TODO + return false; + } + fn flip_column(&mut self, action: Action) ->bool { + //TODO + return false; + } + fn flip_diagonals(&mut self, action: Action) -> bool { + //TODO + return false; } } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Action { - pub color: i8, + pub color: Color, pub x: usize, pub y: usize, } impl Action { - pub fn new(player: i8, x1: usize, y1: usize) -> Self { + pub fn new(player: Color, x1: usize, y1: usize) -> Self { Self { color: player, x: x1, @@ -127,55 +99,68 @@ impl Action { } -pub fn simulate_game(state: &mut State) -> isize { +pub fn simulate_game(state: &State) -> isize { let mut test_state = state.clone(); let mut test_actions = test_state.get_actions(); - let mut do_act: Option; + let mut current_action: Option; while test_state.remaining_moves > 0 { if test_actions.len() < 1 { - do_act = None; + current_action = None; } else { let mut rng = rand::thread_rng(); let index = rng.gen_range(0..test_actions.len()); - do_act = test_actions.get(index).cloned(); + current_action = test_actions.get(index).cloned(); } - test_state = test_state.do_action(do_act); + test_state = test_state.do_action(current_action); test_actions = test_state.get_actions(); } - caculate_win(state.next_turn, test_state) + match caculate_win(test_state) { + Some(Color::WHITE) => 1, + Some(Color::BLACK) => -1, + None => 0 + } } -fn caculate_win(player: i8, state: State) -> isize { - let p1 = player; - let p2 = match p1 { - 1 => 0, - _ => 1, - }; - let mut p1_score: isize = 0; - let mut p2_score: isize = 0; +fn caculate_win(state: State) -> Option { + let mut w_score: isize = 0; + let mut b_score: isize = 0; for row in state.board { - for ch in row { - if ch == p1 { - p1_score += 1; - }else if ch == p2 { - p2_score += 1; - } - } + let (w,b) = count_row(row); + w_score += w; + b_score += b; } - match p1_score - p2_score { - x if x > 0 => 1, - x if x < 0 => -1, - _ => 0, + match w_score - b_score { + x if x > 0 => Some(Color::WHITE), + x if x < 0 => Some(Color::BLACK), + _ => None } } +fn count_row(row: u16) -> (isize, isize) { + let mut w_score = 0; + let mut b_score = 0; + let mut b_pieces = (row & BLACK_BITMASK) >> 1; + let mut w_pieces = row & WHITE_BITMASK; + for _ in 0..BOARD_SIZE { + if w_pieces & 0b1 > 0 { + w_score += 1; + } + if b_pieces & 0b1 > 0 { + b_score += 1; + } + b_pieces = b_pieces >> (1 * FIELD_SIZE); + w_pieces = w_pieces >> (1 * FIELD_SIZE) ; + } + + return (w_score, b_score) +} pub fn parse_state(json: serde_json::Value) -> State { let mut new_board = [[-1;BOARD_SIZE]; BOARD_SIZE]; let mut moves_left: i16 = 0; let next = match json["turn"] { - serde_json::Value::Bool(true) => 1, - _ => 0, + serde_json::Value::Bool(true) => Color::BLACK, + _ => Color::WHITE, }; if let Some(board) = json["board"].as_array() { @@ -195,26 +180,37 @@ pub fn parse_state(json: serde_json::Value) -> State { } } } + State::new() + /* State{ board: new_board, next_turn: next, remaining_moves: moves_left, - } + }*/ } pub fn print_state(state: State) { println!(" 0 1 2 3 4 5 6 7"); for (i, row) in state.board.iter().enumerate() { print!("{i} "); - for ch in row { - let c = match ch { - 1 => 'X', - 0 => '0', - _ => '_', + for f in BOARD_SIZE..0 { + let c = { + if row & (0b10 << (f * FIELD_SIZE)) > 0 { + 'B' + } + else if row & (0b01 << (f *FIELD_SIZE)) > 0 { + 'W' + } else { + '_' + } }; print!("|{}", c); } print!("|\n"); } - println!("Next: {:2}", state.next_turn) + let next = match state.next_turn { + Color::WHITE => "White", + Color::BLACK => "Black" + }; + println!("Next: {}", next) } From b0b59cc31ad86c2132957d9d31f317b5b6472771 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:10:40 +0100 Subject: [PATCH 11/27] chore: DRY --- src/bin/ai-test.rs | 8 ++++++-- src/console_game.rs | 32 ++++++-------------------------- src/othello.rs | 2 +- 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/src/bin/ai-test.rs b/src/bin/ai-test.rs index 372b919..39ff74e 100644 --- a/src/bin/ai-test.rs +++ b/src/bin/ai-test.rs @@ -1,6 +1,6 @@ use std::isize; use rusty_othello_ai::mcts::MCTS; -use rusty_othello_ai::othello::{State,Color}; +use rusty_othello_ai::othello::{State,Color, caculate_win}; pub fn main(){ let args: Vec = std::env::args().collect(); @@ -23,7 +23,11 @@ pub fn main(){ } ai_iterations += ai_iterations / 100; } - win_balance += 1;//determine_winner(state); + win_balance += match caculate_win(state){ + Some(Color::WHITE) => 1, + Some(Color::BLACK) => -1, + None => 0 + }; println!("{win_balance}") } /* diff --git a/src/console_game.rs b/src/console_game.rs index 62f7095..a77ebb4 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -2,7 +2,7 @@ use std::io::Write; use std::isize; use crate::mcts::MCTS; -use crate::othello::{State, Action, Color, print_state}; +use crate::othello::{State, Action, Color, print_state, caculate_win}; pub fn console_game(){ let mut win_balance: isize = 0; @@ -27,34 +27,14 @@ pub fn console_game(){ } } //print_state(state); - win_balance += 1;//determine_winner(state); + win_balance += match caculate_win(state){ + Some(Color::WHITE) => 1, + Some(Color::BLACK) => -1, + None => 0 + }; //println!("\nGAME OVER\n"); println!("\nResult: {win_balance}") } -/* -fn determine_winner(state: State) -> isize { - let p1 = 1; - let p2 = 0; - let mut p1_score: isize = 0; - let mut p2_score: isize = 0; - for row in state.board { - for ch in row { - if ch == p1 { - p1_score += 1; - }else if ch == p2 { - p2_score += 1; - } - } - } - match p1_score - p2_score { - x if x > 0 => 1, - x if x < 0 => -1, - _ => 0, - } - //println!("Score is\t{} {} : {} {}", p1, p1_score, p2_score, p2); - -} -*/ fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { let dev_null = |_a: usize, _b: usize, _c: &Color | -> (){}; diff --git a/src/othello.rs b/src/othello.rs index 7523496..d989cd3 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -122,7 +122,7 @@ pub fn simulate_game(state: &State) -> isize { } } -fn caculate_win(state: State) -> Option { +pub fn caculate_win(state: State) -> Option { let mut w_score: isize = 0; let mut b_score: isize = 0; for row in state.board { From af28e7b7f598cf15cc51163eea116c4475cec798 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:35:16 +0100 Subject: [PATCH 12/27] started bitwise game state implementation --- src/bin/ai-test.rs | 36 +++++++++++--------- src/console_game.rs | 57 +++++++++++++++---------------- src/othello.rs | 81 ++++++++++++++++++++++++++------------------- 3 files changed, 94 insertions(+), 80 deletions(-) diff --git a/src/bin/ai-test.rs b/src/bin/ai-test.rs index 39ff74e..99dcb43 100644 --- a/src/bin/ai-test.rs +++ b/src/bin/ai-test.rs @@ -1,12 +1,20 @@ -use std::isize; use rusty_othello_ai::mcts::MCTS; -use rusty_othello_ai::othello::{State,Color, caculate_win}; +use rusty_othello_ai::othello::{caculate_win, Color, State}; +use std::isize; -pub fn main(){ +pub fn main() { let args: Vec = std::env::args().collect(); let mut win_balance: isize = 0; - let a: f32 = args.get(1).expect("Missing value for A").parse().expect("Not a valid floatingpoint number"); - let b: f32 = args.get(2).expect("Missing value for A").parse().expect("Not a valid floatingpoint number"); + let a: f32 = args + .get(1) + .expect("Missing value for A") + .parse() + .expect("Not a valid floatingpoint number"); + let b: f32 = args + .get(2) + .expect("Missing value for A") + .parse() + .expect("Not a valid floatingpoint number"); let mut state = State::new(); let mut mcts = MCTS::new("true", a); @@ -14,19 +22,19 @@ pub fn main(){ let mut ai_iterations = 500; loop { state = ai_turn(&mut mcts, state.clone(), ai_iterations); - if state.remaining_moves < 0 { + if state.remaining_moves == 0 { break; } state = ai_turn(&mut mcts2, state.clone(), ai_iterations); - if state.remaining_moves < 0 { + if state.remaining_moves == 0 { break; } ai_iterations += ai_iterations / 100; } - win_balance += match caculate_win(state){ + win_balance += match caculate_win(state) { Some(Color::WHITE) => 1, Some(Color::BLACK) => -1, - None => 0 + None => 0, }; println!("{win_balance}") } @@ -43,7 +51,7 @@ fn determine_winner(state: State) -> isize { }else if ch == p2 { p2_score += 1; } - } + } } match p1_score - p2_score { x if x > 0 => 1, @@ -56,13 +64,11 @@ fn determine_winner(state: State) -> isize { */ fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |_a: usize, _b: usize, _c: &Color | -> (){}; - let action = mcts.search(state.clone(), iterations, dev_null); + let dev_null = |_a: usize, _b: usize, _c: &Color| -> () {}; + let action = mcts.search(state.clone(), iterations, dev_null); if action.is_ok() { state.clone().do_action(Some(action.unwrap().clone())) - } - else { + } else { state.clone().do_action(None) } } - diff --git a/src/console_game.rs b/src/console_game.rs index a77ebb4..02b6e16 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -2,9 +2,9 @@ use std::io::Write; use std::isize; use crate::mcts::MCTS; -use crate::othello::{State, Action, Color, print_state, caculate_win}; +use crate::othello::{caculate_win, print_state, Action, Color, State}; -pub fn console_game(){ +pub fn console_game() { let mut win_balance: isize = 0; let a = 1.0; println!("Game mode: player vs AI\n"); @@ -15,40 +15,37 @@ pub fn console_game(){ loop { print_state(state); state = player_turn(state.clone()); - if state.remaining_moves < 0 { + if state.remaining_moves == 0 { break; } print_state(state); state = ai_turn(&mut mcts, state.clone(), ai_iterations); ai_iterations += ai_iterations / 100; - if state.remaining_moves < 0 { + if state.remaining_moves == 0 { break; } } //print_state(state); - win_balance += match caculate_win(state){ + win_balance += match caculate_win(state) { Some(Color::WHITE) => 1, Some(Color::BLACK) => -1, - None => 0 + None => 0, }; //println!("\nGAME OVER\n"); println!("\nResult: {win_balance}") } fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |_a: usize, _b: usize, _c: &Color | -> (){}; - let action = mcts.search(state.clone(), iterations, dev_null); + let dev_null = |_a: usize, _b: usize, _c: &Color| -> () {}; + let action = mcts.search(state.clone(), iterations, dev_null); if action.is_ok() { state.clone().do_action(Some(action.unwrap().clone())) - } - else { + } else { state.clone().do_action(None) } } - - fn player_turn(state: State) -> State { let mut player_choice: Option; let mut buf = String::new(); @@ -58,36 +55,34 @@ fn player_turn(state: State) -> State { let _ = std::io::stdin().read_line(&mut buf); let cmd: Vec<&str> = buf.trim().split(",").clone().collect(); match (cmd.get(0), cmd.get(1)) { - (Some(cmd_1), Some(cmd_2)) => { - match (cmd_1.parse::(), cmd_2.parse::()) { - (Ok(y_index), Ok(x_index)) => { - player_choice = Some(Action { - color: Color::WHITE, - x: x_index, - y: y_index, - }); - if state.get_actions().contains(&player_choice.clone().unwrap()) { - break; - }else { - println!("Invalid move"); - } - }, - _ => println!("Please provide only numbers for indexes"), + (Some(cmd_1), Some(cmd_2)) => match (cmd_1.parse::(), cmd_2.parse::()) { + (Ok(y_index), Ok(x_index)) => { + player_choice = Some(Action { + color: Color::BLACK, + x: x_index, + y: y_index, + }); + if state + .get_actions() + .contains(&player_choice.clone().unwrap()) + { + break; + } else { + println!("Invalid move"); + } } + _ => println!("Please provide only numbers for indexes"), }, (Some(skip), None) => { if skip.to_lowercase() == "skip".to_string() { player_choice = None; break; } - }, + } _ => println!("Please provide a move in the form of 1,2 or \"skip\""), - } buf.clear(); } buf.clear(); state.clone().do_action(player_choice) - } - diff --git a/src/othello.rs b/src/othello.rs index d989cd3..943ad65 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -1,38 +1,47 @@ -use std::{i16, isize, u16, usize}; use rand::Rng; +use std::{isize, u16, usize}; const BOARD_SIZE: usize = 8; const FIELD_SIZE: usize = 2; const BLACK_BITMASK: u16 = 0b1010101010101010; const WHITE_BITMASK: u16 = 0b0101010101010101; +const FLIP_BITMASK: u16 = 0b1100000000000000; -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq,)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Color { BLACK, - WHITE + WHITE, } #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct State { pub board: [u16; BOARD_SIZE], pub next_turn: Color, - pub remaining_moves: i16, + pub remaining_moves: u8, + pub prev_player_skipped: bool, } impl State { - pub fn new() -> Self{ + pub fn new() -> Self { let mut new = Self { board: [0; BOARD_SIZE], next_turn: Color::BLACK, remaining_moves: 60, + prev_player_skipped: false, }; - new.board[3] = 0b11 << 7; + new.board[0] = 0; + new.board[1] = 0; + new.board[2] = 0; + new.board[3] = 0b0110 << 6; new.board[4] = 0b1001 << 6; + new.board[5] = 0; + new.board[6] = 0; + new.board[7] = 0; new } pub fn get_actions(&self) -> Vec { let mut actions: Vec = Vec::new(); let mut _tmp_action = Action::new(self.next_turn, 0, 0); - for _row in self.board.iter(){ + for _row in self.board.iter() { // TODO: Fix this } actions.push(_tmp_action); @@ -45,14 +54,9 @@ impl State { Color::WHITE => Color::BLACK, }; - let mut new_state = State { - next_turn: next_turn.clone(), - board: self.board.clone(), - remaining_moves: (self.remaining_moves.clone()), - }; + let mut new_state = self.clone(); if action.is_some() { - new_state.remaining_moves -= 1; let act = action.unwrap(); new_state.flip_pieces(act); } @@ -60,6 +64,7 @@ impl State { } fn flip_pieces(&mut self, action: Action) -> bool { + assert!(self.next_turn == action.color); let mut result = true; result = result && self.flip_row(action.clone()); result = result && self.flip_column(action.clone()); @@ -67,10 +72,21 @@ impl State { return result; } fn flip_row(&mut self, action: Action) -> bool { + let row = self.board[action.x]; + let offset_right = action.y * FIELD_SIZE; + let offset_left = (BOARD_SIZE * FIELD_SIZE) - offset_right; + let left_of_action = (row >> offset_left) << offset_left; + let right_of_action = (row << offset_right) >> offset_right; + if left_of_action != 0 { + //TODO: Check if valid flip to the left + } + if right_of_action != 0 { + //TODO: Check if valid flip to the left + } //TODO return false; } - fn flip_column(&mut self, action: Action) ->bool { + fn flip_column(&mut self, action: Action) -> bool { //TODO return false; } @@ -80,7 +96,6 @@ impl State { } } - #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Action { pub color: Color, @@ -98,7 +113,6 @@ impl Action { } } - pub fn simulate_game(state: &State) -> isize { let mut test_state = state.clone(); let mut test_actions = test_state.get_actions(); @@ -106,8 +120,7 @@ pub fn simulate_game(state: &State) -> isize { while test_state.remaining_moves > 0 { if test_actions.len() < 1 { current_action = None; - } - else { + } else { let mut rng = rand::thread_rng(); let index = rng.gen_range(0..test_actions.len()); current_action = test_actions.get(index).cloned(); @@ -118,7 +131,7 @@ pub fn simulate_game(state: &State) -> isize { match caculate_win(test_state) { Some(Color::WHITE) => 1, Some(Color::BLACK) => -1, - None => 0 + None => 0, } } @@ -126,14 +139,14 @@ pub fn caculate_win(state: State) -> Option { let mut w_score: isize = 0; let mut b_score: isize = 0; for row in state.board { - let (w,b) = count_row(row); + let (w, b) = count_row(row); w_score += w; b_score += b; } match w_score - b_score { x if x > 0 => Some(Color::WHITE), x if x < 0 => Some(Color::BLACK), - _ => None + _ => None, } } fn count_row(row: u16) -> (isize, isize) { @@ -149,32 +162,31 @@ fn count_row(row: u16) -> (isize, isize) { b_score += 1; } b_pieces = b_pieces >> (1 * FIELD_SIZE); - w_pieces = w_pieces >> (1 * FIELD_SIZE) ; + w_pieces = w_pieces >> (1 * FIELD_SIZE); } - return (w_score, b_score) + return (w_score, b_score); } pub fn parse_state(json: serde_json::Value) -> State { - let mut new_board = [[-1;BOARD_SIZE]; BOARD_SIZE]; - let mut moves_left: i16 = 0; + let mut new_board = [[-1; BOARD_SIZE]; BOARD_SIZE]; + let mut moves_left: u8 = 0; let next = match json["turn"] { serde_json::Value::Bool(true) => Color::BLACK, _ => Color::WHITE, - }; if let Some(board) = json["board"].as_array() { for (x, row) in board.iter().enumerate() { if let Some(row) = row.as_array() { for (y, cell) in row.iter().enumerate() { - match cell.as_i64() { + match cell.as_i64() { Some(1) => new_board[x][y] = 1, Some(0) => new_board[x][y] = 0, Some(-1) => { new_board[x][y] = -1; moves_left += 1; - }, - _ => {}, + } + _ => {} } } } @@ -191,14 +203,15 @@ pub fn parse_state(json: serde_json::Value) -> State { pub fn print_state(state: State) { println!(" 0 1 2 3 4 5 6 7"); + let black_comp = 0b10 << ((BOARD_SIZE - 1) * FIELD_SIZE); + let white_comp = 0b01 << ((BOARD_SIZE - 1) * FIELD_SIZE); for (i, row) in state.board.iter().enumerate() { print!("{i} "); - for f in BOARD_SIZE..0 { + for f in 0..BOARD_SIZE { let c = { - if row & (0b10 << (f * FIELD_SIZE)) > 0 { + if row & (black_comp >> (f * FIELD_SIZE)) != 0 { 'B' - } - else if row & (0b01 << (f *FIELD_SIZE)) > 0 { + } else if row & (white_comp >> (f * FIELD_SIZE)) != 0 { 'W' } else { '_' @@ -210,7 +223,7 @@ pub fn print_state(state: State) { } let next = match state.next_turn { Color::WHITE => "White", - Color::BLACK => "Black" + Color::BLACK => "Black", }; println!("Next: {}", next) } From 607ab2a9db343a5d4dd7e94d95bda7cc36fd7b5a Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:49:25 +0100 Subject: [PATCH 13/27] removed unneeded assignments --- src/othello.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/othello.rs b/src/othello.rs index 943ad65..0403fa9 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -27,14 +27,9 @@ impl State { remaining_moves: 60, prev_player_skipped: false, }; - new.board[0] = 0; - new.board[1] = 0; - new.board[2] = 0; - new.board[3] = 0b0110 << 6; - new.board[4] = 0b1001 << 6; - new.board[5] = 0; - new.board[6] = 0; - new.board[7] = 0; + let center = (BOARD_SIZE / 2) - 1; + new.board[3] = 0b0110 << (center * FIELD_SIZE); + new.board[4] = 0b1001 << (center * FIELD_SIZE); new } From 672483cf971267a0e879baff56a260c5202cd43f Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:42:18 +0100 Subject: [PATCH 14/27] chore: refactored player_turn function for readability --- src/console_game.rs | 81 +++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/src/console_game.rs b/src/console_game.rs index 02b6e16..84402e6 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -1,9 +1,20 @@ use std::io::Write; use std::isize; +use std::process::exit; use crate::mcts::MCTS; use crate::othello::{caculate_win, print_state, Action, Color, State}; +struct PlayerCmd { + pub cmd: GameCommand, +} +enum GameCommand { + SKIP, + QUIT, + INVALID, + MOVE(usize, usize), +} + pub fn console_game() { let mut win_balance: isize = 0; let a = 1.0; @@ -48,41 +59,53 @@ fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { fn player_turn(state: State) -> State { let mut player_choice: Option; - let mut buf = String::new(); loop { print!("Enter coordinates for desired move: "); - let _ = std::io::stdout().flush(); - let _ = std::io::stdin().read_line(&mut buf); - let cmd: Vec<&str> = buf.trim().split(",").clone().collect(); - match (cmd.get(0), cmd.get(1)) { - (Some(cmd_1), Some(cmd_2)) => match (cmd_1.parse::(), cmd_2.parse::()) { - (Ok(y_index), Ok(x_index)) => { - player_choice = Some(Action { - color: Color::BLACK, - x: x_index, - y: y_index, - }); - if state - .get_actions() - .contains(&player_choice.clone().unwrap()) - { - break; - } else { - println!("Invalid move"); - } - } - _ => println!("Please provide only numbers for indexes"), - }, - (Some(skip), None) => { - if skip.to_lowercase() == "skip".to_string() { - player_choice = None; + let cmd = read_command(); + match cmd { + GameCommand::QUIT => exit(0), + GameCommand::INVALID => { + println!("Please provide a valid command 'quit' 'skip' or 'x,y'") + } + GameCommand::SKIP => { + player_choice = None; + break; + } + GameCommand::MOVE(x_index, y_index) => { + player_choice = Some(Action { + color: Color::BLACK, + x: x_index, + y: y_index, + }); + if state + .get_actions() + .contains(&player_choice.clone().unwrap()) + { break; } } - _ => println!("Please provide a move in the form of 1,2 or \"skip\""), } - buf.clear(); } - buf.clear(); state.clone().do_action(player_choice) } + +fn read_command() -> GameCommand { + let mut buf = String::new(); + let _ = std::io::stdin().read_line(&mut buf); + match buf.to_lowercase().as_str() { + "quit" => GameCommand::QUIT, + "skip" => GameCommand::SKIP, + line => { + let cmd: Vec<&str> = line.trim().split(",").clone().collect(); + match (cmd.get(0), cmd.get(1)) { + (Some(cmd_1), Some(cmd_2)) => { + match (cmd_1.parse::(), cmd_2.parse::()) { + (Ok(y_index), Ok(x_index)) => GameCommand::MOVE(x_index, y_index), + _ => GameCommand::INVALID, + } + } + _ => GameCommand::INVALID, + } + } + } +} From 6d2668fead65ad3486ed1e2b76a530aa4bb45fe4 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:47:01 +0100 Subject: [PATCH 15/27] fix: missing io flush and input trim --- src/console_game.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/console_game.rs b/src/console_game.rs index 84402e6..f6f9a2f 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -61,6 +61,7 @@ fn player_turn(state: State) -> State { let mut player_choice: Option; loop { print!("Enter coordinates for desired move: "); + let _ = std::io::stdout().flush(); let cmd = read_command(); match cmd { GameCommand::QUIT => exit(0), @@ -92,7 +93,7 @@ fn player_turn(state: State) -> State { fn read_command() -> GameCommand { let mut buf = String::new(); let _ = std::io::stdin().read_line(&mut buf); - match buf.to_lowercase().as_str() { + match buf.to_lowercase().as_str().trim() { "quit" => GameCommand::QUIT, "skip" => GameCommand::SKIP, line => { From 0f15e90e36f6f8b3c0541929e16b5362f37cdb21 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:41:59 +0200 Subject: [PATCH 16/27] Formatting --- src/lib.rs | 1 - src/main.rs | 47 ++++++++++++--------- src/mcts.rs | 115 ++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 109 insertions(+), 54 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 677bbd5..bd21b18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,2 @@ pub mod mcts; pub mod othello; - diff --git a/src/main.rs b/src/main.rs index 795ff28..e71fab5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,7 @@ mod othello; mod console_game; use console_game::console_game; use mcts::MCTS; -use othello::{State, Action, Color, parse_state}; - - +use othello::{parse_state, Action, Color, State}; const SERVER_URL: &str = "http://localhost:8181"; @@ -20,7 +18,11 @@ fn main() { // If the argument is not recognized, the program will panic let args: Vec = std::env::args().collect(); let ai_color; - match args.get(1).expect("Please specify color to the AI").to_lowercase() { + match args + .get(1) + .expect("Please specify color to the AI") + .to_lowercase() + { x if x == "false" => ai_color = x, x if x == "0" => ai_color = "false".to_string(), x if x == "b" => ai_color = "false".to_string(), @@ -32,7 +34,7 @@ fn main() { x if x == "console" => { console_game(); exit(0); - }, + } _ => panic!("Please pass a proper argument to the AI"), } @@ -47,12 +49,12 @@ fn main() { loop { // The AI checks if it's its turn, if so, it gets the current game state and performs a search using MCTS match is_my_turn(ai_color.borrow()) { - Ok(true) => { + Ok(true) => { state = get_game_state(); choice = mcts.search(state, ai_iterations, send_progress); // Gives the ai 2% more iterations every round to balance the game simulations // being shorter - ai_iterations += ai_iterations / 50; + ai_iterations += ai_iterations / 50; // If a valid action is found, it sends the move to the server and updates the game state if choice.is_ok() { @@ -62,16 +64,15 @@ fn main() { // If no valid action is found, it sends a pass move to the server and updates the game state else { let _ = send_move(&ai_color, None); - state.do_action(None); + state.do_action(None); } - - }, + } // If it's not the AI's turn, it performs a search using MCTS and waits Ok(false) => { - let dev_null = |_a: usize, _b: usize, _c: &Color| -> (){}; + let dev_null = |_a: usize, _b: usize, _c: &Color| -> () {}; _ = mcts.search(state, 1000, dev_null); //sleep(Duration::from_secs(1)); - }, + } Err(e) => { eprintln!("Error checking turn: {}", e); sleep(Duration::from_secs(1)); @@ -85,7 +86,7 @@ fn is_my_turn(ai: &String) -> Result> { let mut delay = Duration::from_secs(1); let opponent = match ai { x if x == "true" => "false", - _ => "true" + _ => "true", }; loop { let url = format!("{}/turn", SERVER_URL); @@ -100,10 +101,13 @@ fn is_my_turn(ai: &String) -> Result> { // If the response is anything else, the function returns an error _ => return Err("Unexpected response from server".into()), } - }, + } Err(e) => { // Error occurred, possibly a network issue or server error, wait before trying again - eprintln!("Error checking turn: {}, will retry after {:?} seconds", e, delay); + eprintln!( + "Error checking turn: {}, will retry after {:?} seconds", + e, delay + ); sleep(delay); delay = std::cmp::min(delay.saturating_mul(2), Duration::from_secs(10)); } @@ -118,12 +122,14 @@ fn get_game_state() -> State { let mut delay = Duration::from_secs(3); loop { match get_json() { - Ok(resp) => return parse_state(resp.into_json().expect("Error parsing response to json")), + Ok(resp) => { + return parse_state(resp.into_json().expect("Error parsing response to json")) + } Err(_e) => { sleep(delay); delay *= 2; delay = std::cmp::min(Duration::from_millis(10000), delay); - }, + } } } } @@ -144,7 +150,10 @@ fn send_move(player: &String, ai_move: Option) -> Result) -> Result "false", Color::WHITE => "true", diff --git a/src/mcts.rs b/src/mcts.rs index 3390481..3972cc6 100644 --- a/src/mcts.rs +++ b/src/mcts.rs @@ -1,5 +1,4 @@ - -use crate::othello::{State, Action, Color, simulate_game}; +use crate::othello::{simulate_game, Action, Color, State}; use std::collections::HashMap; @@ -13,7 +12,7 @@ pub struct Node { } impl Node { - pub fn new (state: State, action: Option, untried_actions: Vec) -> Node { + pub fn new(state: State, action: Option, untried_actions: Vec) -> Node { Node { state, action, @@ -23,7 +22,7 @@ impl Node { } } - pub fn update_node(&mut self, result: (Color, isize)) { + pub fn update_node(&mut self, result: (Color, isize)) { self.visits += 1; if result.0 == self.state.next_turn { self.score += result.1; @@ -33,7 +32,8 @@ impl Node { } // Calculates and returns the Upper Confidence Bound (UCB) for the Node fn calculate_ucb(&self, total_count: usize, explore: f32) -> f32 { - (self.score as f32 / self.visits as f32) + explore * (2.0 * (total_count as f32).ln() / self.visits as f32).sqrt() + (self.score as f32 / self.visits as f32) + + explore * (2.0 * (total_count as f32).ln() / self.visits as f32).sqrt() } } @@ -71,31 +71,41 @@ impl MCTS { // Performs a Monte Carlo Tree Search from the given state for the given number of iterations // It returns the best action found or an error if no action was found - pub fn search(&mut self, from: State, iterations: usize, send_status: fn(usize, usize, &Color)) -> Result { + pub fn search( + &mut self, + from: State, + iterations: usize, + send_status: fn(usize, usize, &Color), + ) -> Result { if let Some(root) = self.state_map.get(&from).cloned() { for i in 0..iterations { if i % 1000 == 0 { //println!("Progress: {i}/{iterations}"); - _ = send_status(i, iterations, &self.color); + _ = send_status(i, iterations, &self.color); } let node_index = self.select(root.clone()).clone(); let node_index = self.expand(node_index.clone()).clone(); - for index in self.tree.get(node_index).expect("No child nodes to simulate").clone().iter() { + for index in self + .tree + .get(node_index) + .expect("No child nodes to simulate") + .clone() + .iter() + { let result: (Color, isize) = self.simulate(*index); self.backpropagate(*index, result.clone()); } } Ok(self.get_best_choice(root)?) - } - else { + } else { self.add_node(from.clone(), None, None); - return self.search(from, iterations, send_status) + return self.search(from, iterations, send_status); } } // Adds a new node to the MCTS with the given state, action, and parent - fn add_node(&mut self, state: State, action: Option, parent: Option){ + fn add_node(&mut self, state: State, action: Option, parent: Option) { let new_node = Node::new(state, action, state.get_actions()); self.state_map.insert(state, self.size); self.tree.push(Vec::new()); @@ -110,20 +120,32 @@ impl MCTS { let mut max_index = 0 as usize; let mut node_index = root_index; loop { - if self.tree.get(node_index).expect("Empty child selection").len() == 0 { + if self + .tree + .get(node_index) + .expect("Empty child selection") + .len() + == 0 + { return node_index; - } - else { + } else { for index in self.tree.get(node_index).unwrap().iter() { - let node = self.nodes.get(*index).expect("selected child doesnt exist").clone(); - let node_ucb = node.calculate_ucb(self.nodes.get(node_index).unwrap().visits as usize, self.expl); + let node = self + .nodes + .get(*index) + .expect("selected child doesnt exist") + .clone(); + let node_ucb = node.calculate_ucb( + self.nodes.get(node_index).unwrap().visits as usize, + self.expl, + ); if node_ucb > max_ucb { max_ucb = node_ucb; max_index = index.clone(); } } node_index = max_index; - } + } max_ucb = std::f32::MIN; max_index = 0; } @@ -131,21 +153,32 @@ impl MCTS { // Expands the given node in the MCTS by adding all its untried actions as new nodes fn expand(&mut self, node_index: usize) -> usize { - let mut node = self.nodes.get_mut(node_index).expect("No node to expand").clone(); + let mut node = self + .nodes + .get_mut(node_index) + .expect("No node to expand") + .clone(); if node.untried_actions.len() == 0 { - self.add_node(node.state.clone().do_action(None), - None, - Some(node_index.clone()) + self.add_node( + node.state.clone().do_action(None), + None, + Some(node_index.clone()), ); - self.tree.get_mut(node_index).expect("No node").push(self.size - 1); + self.tree + .get_mut(node_index) + .expect("No node") + .push(self.size - 1); } else { for (_i, action) in node.untried_actions.iter().enumerate() { self.add_node( - node.state.clone().do_action(Some(action.clone())), - Some(action.clone()), - Some(node_index.clone()) + node.state.clone().do_action(Some(action.clone())), + Some(action.clone()), + Some(node_index.clone()), ); - self.tree.get_mut(node_index).expect("No node").push(self.size - 1); + self.tree + .get_mut(node_index) + .expect("No node") + .push(self.size - 1); } while node.untried_actions.len() > 0 { node.untried_actions.pop(); @@ -171,12 +204,18 @@ impl MCTS { // Updates the nodes in the MCTS from the given child node to the root based on the result of a simulated game fn backpropagate(&mut self, child_index: usize, result: (Color, isize)) { let mut current_node: &mut Node; - let mut parent_index: Option = self.parents.get(child_index).unwrap().clone(); + let mut parent_index: Option = self.parents.get(child_index).unwrap().clone(); while parent_index.is_some() { - current_node = self.nodes.get_mut(parent_index.unwrap()).expect("Parent doesn't exist"); + current_node = self + .nodes + .get_mut(parent_index.unwrap()) + .expect("Parent doesn't exist"); current_node.update_node(result); let tmp = parent_index.clone(); - parent_index = *self.parents.get(tmp.unwrap()).expect("Error fetching parent of parent"); + parent_index = *self + .parents + .get(tmp.unwrap()) + .expect("Error fetching parent of parent"); } } @@ -185,8 +224,17 @@ impl MCTS { fn get_best_choice(&self, from_index: usize) -> Result { let mut best_index = 0; let mut max_visits = 0; - for index in self.tree.get(from_index).expect("Empty list of children when getting best choice").iter().clone() { - let node = self.nodes.get(*index).expect("MCST, choice: node index doesnt exists"); + for index in self + .tree + .get(from_index) + .expect("Empty list of children when getting best choice") + .iter() + .clone() + { + let node = self + .nodes + .get(*index) + .expect("MCST, choice: node index doesnt exists"); if node.visits > max_visits { best_index = index.clone(); max_visits = node.visits; @@ -200,8 +248,7 @@ impl MCTS { let from_state = self.nodes.get(from_index).unwrap().clone().state; if from_state.next_turn != best_action.color { return Err(()); - } - else { + } else { Ok(best_action.clone()) } } From f9a9a9fd44ffe22bd5e0423b8386427e0a5ecaba Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:43:55 +0200 Subject: [PATCH 17/27] Formatting --- src/main.rs | 6 ------ src/mcts.rs | 1 - 2 files changed, 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index e71fab5..ab5e305 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,7 @@ -use ureq::Response; use std::process::exit; -use std::usize; -use std::{thread::sleep, borrow::Borrow}; use std::time::Duration; mod mcts; mod othello; -mod console_game; use console_game::console_game; use mcts::MCTS; use othello::{parse_state, Action, Color, State}; @@ -36,7 +32,6 @@ fn main() { exit(0); } _ => panic!("Please pass a proper argument to the AI"), - } // Initialize the game state and the Monte Carlo Tree Search (MCTS) // The MCTS is initialized with a new node that represents the current game state @@ -170,5 +165,4 @@ fn send_progress(current: usize, total: usize, ai_color: &Color) { }; let url = format!("{}/AIStatus/{}/{}/{}", SERVER_URL, current, total, color); _ = ureq::post(&url).call(); - } diff --git a/src/mcts.rs b/src/mcts.rs index 3972cc6..d44af20 100644 --- a/src/mcts.rs +++ b/src/mcts.rs @@ -95,7 +95,6 @@ impl MCTS { let result: (Color, isize) = self.simulate(*index); self.backpropagate(*index, result.clone()); } - } Ok(self.get_best_choice(root)?) } else { From dec80b646a408c0b9b20e39d63b9d455ebe573cf Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:44:41 +0200 Subject: [PATCH 18/27] Formatting --- src/main.rs | 4 ++++ src/mcts.rs | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index ab5e305..300adde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,9 @@ use std::process::exit; use std::time::Duration; +use std::usize; +use std::{borrow::Borrow, thread::sleep}; +use ureq::Response; +mod console_game; mod mcts; mod othello; use console_game::console_game; diff --git a/src/mcts.rs b/src/mcts.rs index d44af20..d1ba301 100644 --- a/src/mcts.rs +++ b/src/mcts.rs @@ -1,7 +1,6 @@ use crate::othello::{simulate_game, Action, Color, State}; use std::collections::HashMap; - #[derive(Debug, Clone)] pub struct Node { state: State, @@ -35,7 +34,6 @@ impl Node { (self.score as f32 / self.visits as f32) + explore * (2.0 * (total_count as f32).ln() / self.visits as f32).sqrt() } - } #[derive()] @@ -252,4 +250,3 @@ impl MCTS { } } } - From 0ad752ea772a9b4cc8daccb60434ef9c05f7c293 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:45:40 +0200 Subject: [PATCH 19/27] removed unused PlayerCommand struct --- src/console_game.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/console_game.rs b/src/console_game.rs index f6f9a2f..be1b022 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -5,9 +5,6 @@ use std::process::exit; use crate::mcts::MCTS; use crate::othello::{caculate_win, print_state, Action, Color, State}; -struct PlayerCmd { - pub cmd: GameCommand, -} enum GameCommand { SKIP, QUIT, From f244e9f0ff680ea73b909954b1d7418dd164cc56 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:51:46 +0200 Subject: [PATCH 20/27] finished bitwise game state implementation --- src/othello.rs | 506 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 410 insertions(+), 96 deletions(-) diff --git a/src/othello.rs b/src/othello.rs index 0403fa9..5583922 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -1,127 +1,431 @@ use rand::Rng; -use std::{isize, u16, usize}; +use std::{fmt, isize, u16, usize}; const BOARD_SIZE: usize = 8; const FIELD_SIZE: usize = 2; -const BLACK_BITMASK: u16 = 0b1010101010101010; -const WHITE_BITMASK: u16 = 0b0101010101010101; -const FLIP_BITMASK: u16 = 0b1100000000000000; + +#[derive(Debug, Clone)] +struct EmptyFieldError; +impl fmt::Display for EmptyFieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Empty Fields can't be flipped") + } +} +#[derive(Debug, Clone)] +struct OccupiedFieldError; +impl fmt::Display for OccupiedFieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Occupied Fields can't be Set") + } +} #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Color { BLACK, WHITE, } +impl Color { + fn bitmask(&self) -> u16 { + match *self { + Color::BLACK => 0b010, + Color::WHITE => 0b001, + } + } +} +#[derive(Debug, Clone, Copy)] +pub enum Direction { + Left, + Right, + Up, + Down, + UpLeft, + UpRight, + DownLeft, + DownRight, +} +impl Direction { + const VALUES: [Self; 8] = [ + Self::Left, + Self::Right, + Self::Up, + Self::Down, + Self::UpLeft, + Self::UpRight, + Self::DownLeft, + Self::DownRight, + ]; +} +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub struct Position { + pub x: usize, + pub y: usize, +} +impl Position { + fn new(x_coordinate: usize, y_coordinate: usize) -> Option { + match (x_coordinate, y_coordinate) { + (x, y) if x >= BOARD_SIZE || y >= BOARD_SIZE => None, + (_, _) => Some(Self { + x: x_coordinate, + y: y_coordinate, + }), + } + } + fn shift(self, dir: Direction) -> Option { + let x = self.x; + let y = self.y; + match dir { + Direction::Up => match y { + 0 => None, + _ => Position::new(x, y - 1), + }, + Direction::Down => match y + 1 { + BOARD_SIZE => None, + _ => Position::new(x, y + 1), + }, + Direction::Left => match x { + 0 => None, + _ => Position::new(x - 1, y), + }, + Direction::Right => match x + 1 { + BOARD_SIZE => None, + _ => Position::new(x + 1, y), + }, + Direction::UpLeft => match (x, y) { + (0, _) => None, + (_, 0) => None, + (_, _) => Position::new(x - 1, y - 1), + }, + Direction::UpRight => match (x + 1, y) { + (BOARD_SIZE, _) => None, + (_, 0) => None, + (_, _) => Position::new(x + 1, y - 1), + }, + Direction::DownLeft => match (x, y + 1) { + (0, _) => None, + (_, BOARD_SIZE) => None, + (_, _) => Position::new(x - 1, y + 1), + }, + Direction::DownRight => match (x, y) { + (BOARD_SIZE, _) => None, + (_, BOARD_SIZE) => None, + (_, _) => Position::new(x + 1, y + 1), + }, + } + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +struct Row { + value: u16, +} +impl Row { + fn new(val: u16) -> Row { + Self { value: val } + } + fn get_pos(&self, pos: usize) -> Option { + let mask = 0b011 << (pos * FIELD_SIZE); + let field = (self.value & mask) >> (pos * FIELD_SIZE); + match field { + w if w == Color::WHITE.bitmask() => Some(Color::WHITE), + b if b == Color::BLACK.bitmask() => Some(Color::BLACK), + _ => None, + } + } + fn set_pos(&self, color: Color, pos: usize) -> Result { + let color_mask = color.bitmask() << (pos * FIELD_SIZE); + let check_mask = 0b011 << (pos * FIELD_SIZE); + match self.value & check_mask { + 0 => Ok(Row { + value: self.value ^ color_mask, + }), + _ => Err(OccupiedFieldError), + } + } + fn flip_pos(&self, pos: usize) -> Result { + let flip_mask = 0b011 << (pos * FIELD_SIZE); + match self.value & flip_mask { + 0 => Err(EmptyFieldError), + _ => Ok(Row { + value: self.value ^ flip_mask, + }), + } + } + fn count_colors(&self) -> (isize, isize) { + let mut w_score = 0; + let mut b_score = 0; + let mut row = self.value.clone(); + for _ in 0..BOARD_SIZE { + if row & Color::WHITE.bitmask() > 0 { + w_score += 1; + } + if row & Color::BLACK.bitmask() > 0 { + b_score += 1; + } + row = row >> FIELD_SIZE; + } + return (w_score, b_score); + } +} +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +struct Board { + rows: [Row; BOARD_SIZE as usize], +} +impl Board { + fn new() -> Board { + let mut new_rows = [Row::new(0); BOARD_SIZE as usize]; + let center = (BOARD_SIZE / 2) - 1; + new_rows[center as usize] = Row::new(0b1001 << (center * FIELD_SIZE)); + new_rows[(center + 1) as usize] = Row::new(0b0110 << (center * FIELD_SIZE)); + Self { rows: new_rows } + } + fn flip_pieces(&self, action: Action, position: Position, dir: Direction) -> Option { + let mut to_flip = Vec::new(); + let mut current_pos = position; + + // Move in the specified direction, collecting opponent pieces + while let Some(next_pos) = current_pos.shift(dir) { + match self.rows[next_pos.y].get_pos(next_pos.x) { + Some(color) if color != action.color => { + // Found an opponent's piece add it to list + to_flip.push(next_pos); + current_pos = next_pos; + } + Some(color) if color == action.color => { + // Found own piece flip all the pieces collected + if !to_flip.is_empty() { + // Create new board with the flipped pieces + let mut new_board = self.clone(); + + // Flip all pieces in between + for pos in to_flip { + new_board.rows[pos.y] = new_board.rows[pos.y] + .flip_pos(pos.x) + .expect("Should be able to flip occupied positions"); + } + + return Some(new_board); + } + return None; + } + _ => { + // Empty space or board edge, can't flip in this direction + return None; + } + } + } + None + } + fn get_empty_positions(&self) -> Vec { + let mut positions = Vec::new(); + for (y, row) in self.into_iter().enumerate() { + for x in 0..BOARD_SIZE { + match row.get_pos(x) { + None => { + positions.push(Position::new(x, y).expect( + "Iterating through board shouldn't be able to get out of bounds", + )) + } + Some(_) => (), + } + } + } + return positions; + } + fn would_flip_pieces(&self, action: Action, position: Position, dir: Direction) -> bool { + match position.shift(dir) { + Some(pos_1) => match self.rows[pos_1.y].get_pos(pos_1.x) { + Some(color) if color != action.color => { + // Found an opponent's piece in this direction + let mut current_pos = pos_1; + while let Some(next_pos) = current_pos.shift(dir) { + match self.rows[next_pos.y].get_pos(next_pos.x) { + Some(color) if color == action.color => { + // Found our own piece on the other side + return true; + } + Some(_) => { + // Another opponent piece keep checking + current_pos = next_pos; + } + None => { + // Empty space can't flip + return false; + } + } + } + false // Reached edge of board without finding own piece + } + _ => false, // Either empty or same color + }, + None => false, // Can't go in this direction + } + } +} +impl IntoIterator for Board { + type Item = Row; + type IntoIter = BoardIntoIterator; + fn into_iter(self) -> Self::IntoIter { + BoardIntoIterator { + board: self.clone(), + index: 0, + } + } +} +struct BoardIntoIterator { + board: Board, + index: usize, +} +impl Iterator for BoardIntoIterator { + type Item = Row; + fn next(&mut self) -> Option { + let result = match self.index { + x if x < BOARD_SIZE as usize => self.board.rows[x], + _ => return None, + }; + self.index += 1; + Some(result) + } +} + #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct State { - pub board: [u16; BOARD_SIZE], + board: Board, pub next_turn: Color, pub remaining_moves: u8, pub prev_player_skipped: bool, } impl State { pub fn new() -> Self { - let mut new = Self { - board: [0; BOARD_SIZE], + Self { + board: Board::new(), next_turn: Color::BLACK, - remaining_moves: 60, + remaining_moves: 121, prev_player_skipped: false, - }; - let center = (BOARD_SIZE / 2) - 1; - new.board[3] = 0b0110 << (center * FIELD_SIZE); - new.board[4] = 0b1001 << (center * FIELD_SIZE); - new + } } - pub fn get_actions(&self) -> Vec { - let mut actions: Vec = Vec::new(); - let mut _tmp_action = Action::new(self.next_turn, 0, 0); - for _row in self.board.iter() { - // TODO: Fix this + let empty_spots = self.board.get_empty_positions(); + let mut actions = Vec::new(); + if empty_spots.len() == 0 { + return actions; + } + for pos in empty_spots { + let action = Action::new(self.next_turn.clone(), pos); + if self.is_valid_action(action.clone()) { + actions.push(action); + } } - actions.push(_tmp_action); return actions; } + fn is_valid_action(&self, action: Action) -> bool { + for dir in Direction::VALUES { + if self + .board + .would_flip_pieces(action.clone(), action.position.clone(), dir) + { + return true; + } + } + false + } pub fn do_action(&self, action: Option) -> State { - let next_turn = match self.next_turn { + let mut new_state = self.clone(); + match action { + Some(act) => { + if new_state.flip_directions(act) { + new_state.remaining_moves -= 1; + new_state.prev_player_skipped = false; + } else { + new_state.prev_player_skipped = true; + } + } + None => { + new_state.prev_player_skipped = true; + } + } + // If both players had to skip end the game + if new_state.prev_player_skipped && self.prev_player_skipped { + new_state.remaining_moves = 0; + } + new_state.next_turn = match self.next_turn { Color::BLACK => Color::WHITE, Color::WHITE => Color::BLACK, }; - - let mut new_state = self.clone(); - - if action.is_some() { - let act = action.unwrap(); - new_state.flip_pieces(act); - } - return new_state; + new_state } + fn flip_directions(&mut self, action: Action) -> bool { + let mut any_flipped = false; + let mut new_board = self.board.clone(); - fn flip_pieces(&mut self, action: Action) -> bool { - assert!(self.next_turn == action.color); - let mut result = true; - result = result && self.flip_row(action.clone()); - result = result && self.flip_column(action.clone()); - result = result && self.flip_diagonals(action); - return result; - } - fn flip_row(&mut self, action: Action) -> bool { - let row = self.board[action.x]; - let offset_right = action.y * FIELD_SIZE; - let offset_left = (BOARD_SIZE * FIELD_SIZE) - offset_right; - let left_of_action = (row >> offset_left) << offset_left; - let right_of_action = (row << offset_right) >> offset_right; - if left_of_action != 0 { - //TODO: Check if valid flip to the left + // Set the piece at the action position + if let Ok(row) = new_board.rows[action.position.y].set_pos(action.color, action.position.x) + { + new_board.rows[action.position.y] = row; + } else { + return false; + } + // Check each direction for pieces to flip + for dir in Direction::VALUES { + if let Some(updated_board) = new_board.flip_pieces(action.clone(), action.position, dir) + { + new_board = updated_board; + any_flipped = true; + } } - if right_of_action != 0 { - //TODO: Check if valid flip to the left + if any_flipped { + self.board = new_board; + self.remaining_moves -= 1; } - //TODO - return false; - } - fn flip_column(&mut self, action: Action) -> bool { - //TODO - return false; - } - fn flip_diagonals(&mut self, action: Action) -> bool { - //TODO - return false; + any_flipped } } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Action { pub color: Color, - pub x: usize, - pub y: usize, + pub position: Position, } impl Action { - pub fn new(player: Color, x1: usize, y1: usize) -> Self { + pub fn new(player: Color, pos: Position) -> Self { Self { color: player, - x: x1, - y: y1, + position: pos, } } } pub fn simulate_game(state: &State) -> isize { let mut test_state = state.clone(); - let mut test_actions = test_state.get_actions(); - let mut current_action: Option; - while test_state.remaining_moves > 0 { - if test_actions.len() < 1 { + let mut consecutive_skips = 0; + + // Maximum number of moves to prevent infinite loops + let max_iterations = 100; + let mut iterations = 0; + + while test_state.remaining_moves > 0 && consecutive_skips < 2 && iterations < max_iterations { + iterations += 1; + + let test_actions = test_state.get_actions(); + let current_action; + + if test_actions.is_empty() { current_action = None; + consecutive_skips += 1; } else { let mut rng = rand::thread_rng(); let index = rng.gen_range(0..test_actions.len()); - current_action = test_actions.get(index).cloned(); + current_action = Some(test_actions[index].clone()); + consecutive_skips = 0; } + test_state = test_state.do_action(current_action); - test_actions = test_state.get_actions(); + + // If both players had to skip end the game + if consecutive_skips >= 2 { + break; + } } match caculate_win(test_state) { Some(Color::WHITE) => 1, @@ -133,8 +437,8 @@ pub fn simulate_game(state: &State) -> isize { pub fn caculate_win(state: State) -> Option { let mut w_score: isize = 0; let mut b_score: isize = 0; - for row in state.board { - let (w, b) = count_row(row); + for row in state.board.rows { + let (w, b) = row.count_colors(); w_score += w; b_score += b; } @@ -144,29 +448,13 @@ pub fn caculate_win(state: State) -> Option { _ => None, } } -fn count_row(row: u16) -> (isize, isize) { - let mut w_score = 0; - let mut b_score = 0; - let mut b_pieces = (row & BLACK_BITMASK) >> 1; - let mut w_pieces = row & WHITE_BITMASK; - for _ in 0..BOARD_SIZE { - if w_pieces & 0b1 > 0 { - w_score += 1; - } - if b_pieces & 0b1 > 0 { - b_score += 1; - } - b_pieces = b_pieces >> (1 * FIELD_SIZE); - w_pieces = w_pieces >> (1 * FIELD_SIZE); - } - - return (w_score, b_score); -} pub fn parse_state(json: serde_json::Value) -> State { + todo!("Fix parse_state") + /* TODO: Fix let mut new_board = [[-1; BOARD_SIZE]; BOARD_SIZE]; let mut moves_left: u8 = 0; - let next = match json["turn"] { + let _next = match json["turn"] { serde_json::Value::Bool(true) => Color::BLACK, _ => Color::WHITE, }; @@ -187,8 +475,6 @@ pub fn parse_state(json: serde_json::Value) -> State { } } } - State::new() - /* State{ board: new_board, next_turn: next, @@ -198,15 +484,15 @@ pub fn parse_state(json: serde_json::Value) -> State { pub fn print_state(state: State) { println!(" 0 1 2 3 4 5 6 7"); - let black_comp = 0b10 << ((BOARD_SIZE - 1) * FIELD_SIZE); - let white_comp = 0b01 << ((BOARD_SIZE - 1) * FIELD_SIZE); - for (i, row) in state.board.iter().enumerate() { + let black_comp = Color::BLACK.bitmask(); // << ((BOARD_SIZE - 1) * FIELD_SIZE); + let white_comp = Color::WHITE.bitmask(); // << ((BOARD_SIZE - 1) * FIELD_SIZE); + for (i, row) in state.board.into_iter().enumerate() { print!("{i} "); for f in 0..BOARD_SIZE { let c = { - if row & (black_comp >> (f * FIELD_SIZE)) != 0 { + if row.value & (black_comp << (f * FIELD_SIZE)) != 0 { 'B' - } else if row & (white_comp >> (f * FIELD_SIZE)) != 0 { + } else if row.value & (white_comp << (f * FIELD_SIZE)) != 0 { 'W' } else { '_' @@ -217,8 +503,36 @@ pub fn print_state(state: State) { print!("|\n"); } let next = match state.next_turn { - Color::WHITE => "White", Color::BLACK => "Black", + Color::WHITE => "White", }; println!("Next: {}", next) } + +#[cfg(test)] +mod OthelloTests { + use super::*; + + #[test] + fn test_board_empty_spaces() { + let board = Board::new(); + assert_eq!(board.get_empty_positions().len(), 60); + } + #[test] + fn test_row_get_pos() { + let board = Board::new(); + assert_eq!(board.rows[3].get_pos(3), Some(Color::WHITE)); + assert_eq!(board.rows[3].get_pos(4), Some(Color::BLACK)); + assert_eq!(board.rows[4].get_pos(4), Some(Color::WHITE)); + assert_eq!(board.rows[4].get_pos(3), Some(Color::BLACK)); + assert_eq!(board.rows[1].get_pos(3), None); + assert_eq!(board.rows[2].get_pos(2), None); + assert_eq!(board.rows[2].get_pos(4), None); + } + #[test] + fn test_row_set_pos() { + let board = Board::new(); + assert!(board.rows[3].set_pos(Color::BLACK, 4).is_err()); + assert!(board.rows[3].set_pos(Color::WHITE, 3).is_err()); + } +} From 8c686a8af2e0be168828a8ff4575365515a0d525 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:52:31 +0200 Subject: [PATCH 21/27] fixed edge cases --- src/mcts.rs | 129 ++++++++++++++++++++++++---------------------------- 1 file changed, 59 insertions(+), 70 deletions(-) diff --git a/src/mcts.rs b/src/mcts.rs index d1ba301..3de11d2 100644 --- a/src/mcts.rs +++ b/src/mcts.rs @@ -1,4 +1,5 @@ use crate::othello::{simulate_game, Action, Color, State}; +use rand::Rng; use std::collections::HashMap; #[derive(Debug, Clone)] @@ -54,8 +55,6 @@ impl MCTS { b if b == "false".to_string() => ai_color = Color::BLACK, _ => ai_color = Color::WHITE, }; - //let mut map = HashMap::new(); - //map.insert(node.state, 0 as usize); Self { tree: Vec::new(), color: ai_color, @@ -78,23 +77,14 @@ impl MCTS { if let Some(root) = self.state_map.get(&from).cloned() { for i in 0..iterations { if i % 1000 == 0 { - //println!("Progress: {i}/{iterations}"); _ = send_status(i, iterations, &self.color); } - let node_index = self.select(root.clone()).clone(); - let node_index = self.expand(node_index.clone()).clone(); - for index in self - .tree - .get(node_index) - .expect("No child nodes to simulate") - .clone() - .iter() - { - let result: (Color, isize) = self.simulate(*index); - self.backpropagate(*index, result.clone()); - } + let selected_node = self.select(root); + let expanded_node = self.expand(selected_node); + let result: (Color, isize) = self.simulate(expanded_node); + self.backpropagate(expanded_node, result); } - Ok(self.get_best_choice(root)?) + return self.get_best_choice(root); } else { self.add_node(from.clone(), None, None); return self.search(from, iterations, send_status); @@ -116,72 +106,71 @@ impl MCTS { let mut max_ucb = std::f32::MIN; let mut max_index = 0 as usize; let mut node_index = root_index; + let mut depth = 0; loop { - if self - .tree - .get(node_index) - .expect("Empty child selection") - .len() - == 0 - { + // Failsafe to avoid tree becoming too deep + if depth > 100 { + return node_index; + } + let children = &self.tree[node_index]; + if children.is_empty() { + return node_index; + } + if !self.nodes[node_index].untried_actions.is_empty() { return node_index; - } else { - for index in self.tree.get(node_index).unwrap().iter() { - let node = self - .nodes - .get(*index) - .expect("selected child doesnt exist") - .clone(); - let node_ucb = node.calculate_ucb( - self.nodes.get(node_index).unwrap().visits as usize, - self.expl, - ); - if node_ucb > max_ucb { - max_ucb = node_ucb; - max_index = index.clone(); - } + } + let parent_visits = self.nodes[node_index].visits; + for &child_index in children { + let child = &self.nodes[child_index]; + let ucb = child.calculate_ucb(parent_visits, self.expl); + + if ucb > max_ucb { + max_ucb = ucb; + max_index = child_index; } - node_index = max_index; } + if max_index == node_index { + return node_index; + } + node_index = max_index; max_ucb = std::f32::MIN; - max_index = 0; + depth += 1; } } // Expands the given node in the MCTS by adding all its untried actions as new nodes fn expand(&mut self, node_index: usize) -> usize { - let mut node = self - .nodes - .get_mut(node_index) - .expect("No node to expand") - .clone(); - if node.untried_actions.len() == 0 { - self.add_node( - node.state.clone().do_action(None), - None, - Some(node_index.clone()), - ); - self.tree - .get_mut(node_index) - .expect("No node") - .push(self.size - 1); + // Get the node not a clone of it + let untried_actions = self.nodes[node_index].untried_actions.clone(); + + if untried_actions.is_empty() { + // No actions to try add skip node + let new_state = self.nodes[node_index].state.clone().do_action(None); + self.add_node(new_state, None, Some(node_index)); + self.tree[node_index].push(self.size - 1); + + // Return the new node's index + return self.size - 1; } else { - for (_i, action) in node.untried_actions.iter().enumerate() { - self.add_node( - node.state.clone().do_action(Some(action.clone())), - Some(action.clone()), - Some(node_index.clone()), - ); - self.tree - .get_mut(node_index) - .expect("No node") - .push(self.size - 1); - } - while node.untried_actions.len() > 0 { - node.untried_actions.pop(); - } + // Pick one random action to expand (not all at once) + let mut rng = rand::thread_rng(); + let action_index = rng.gen_range(0..untried_actions.len()); + let action = untried_actions[action_index].clone(); + + // Remove this action from untried_actions in the original node + self.nodes[node_index].untried_actions.remove(action_index); + + // Create a new node with this action + let new_state = self.nodes[node_index] + .state + .clone() + .do_action(Some(action.clone())); + self.add_node(new_state, Some(action), Some(node_index)); + self.tree[node_index].push(self.size - 1); + + // Return the new node's index + return self.size - 1; } - node_index } // Simulates a game from the given node and returns the result From a700a2f7d6ee66b3efc55abec28c0ce1ee7f97a2 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:53:10 +0200 Subject: [PATCH 22/27] updated to match bitwise game state implementation --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 300adde..45f4f9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,7 +151,7 @@ fn send_move(player: &String, ai_move: Option) -> Result Date: Fri, 25 Apr 2025 16:54:40 +0200 Subject: [PATCH 23/27] improved player experience --- src/console_game.rs | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/console_game.rs b/src/console_game.rs index be1b022..6c69ff9 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -3,7 +3,7 @@ use std::isize; use std::process::exit; use crate::mcts::MCTS; -use crate::othello::{caculate_win, print_state, Action, Color, State}; +use crate::othello::{caculate_win, print_state, Action, Color, Position, State}; enum GameCommand { SKIP, @@ -19,7 +19,7 @@ pub fn console_game() { let mut state = State::new(); let mut mcts = MCTS::new("true", a); _ = std::io::stdout().flush(); - let mut ai_iterations = 5000; + let mut ai_iterations = 20000; loop { print_state(state); state = player_turn(state.clone()); @@ -36,18 +36,28 @@ pub fn console_game() { } //print_state(state); win_balance += match caculate_win(state) { - Some(Color::WHITE) => 1, - Some(Color::BLACK) => -1, - None => 0, + Some(Color::WHITE) => { + println!("White wins!"); + 1 + } + Some(Color::BLACK) => { + println!("Black wins!"); + -1 + } + None => { + println!("Draw."); + 0 + } }; //println!("\nGAME OVER\n"); println!("\nResult: {win_balance}") } fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |_a: usize, _b: usize, _c: &Color| -> () {}; + let dev_null = |a: usize, b: usize, _c: &Color| -> () { /*println!("Progress: {a}/{b}")*/ }; let action = mcts.search(state.clone(), iterations, dev_null); if action.is_ok() { + println!("{:?}", action.clone().unwrap().position); state.clone().do_action(Some(action.unwrap().clone())) } else { state.clone().do_action(None) @@ -55,7 +65,7 @@ fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { } fn player_turn(state: State) -> State { - let mut player_choice: Option; + let mut player_choice; loop { print!("Enter coordinates for desired move: "); let _ = std::io::stdout().flush(); @@ -72,14 +82,25 @@ fn player_turn(state: State) -> State { GameCommand::MOVE(x_index, y_index) => { player_choice = Some(Action { color: Color::BLACK, - x: x_index, - y: y_index, + position: Position { + x: x_index, + y: y_index, + }, }); if state .get_actions() .contains(&player_choice.clone().unwrap()) { break; + } else { + println!("Invalid move."); + let pos: Vec<(usize, usize)> = state + .get_actions() + .iter() + .map(|a| (a.position.y, a.position.x)) + .collect(); + println!("Valid moves: {:?}", pos); + print_state(state); } } } From 46da84433c29909bf8d661986286e1cd0a4a0481 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:55:40 +0200 Subject: [PATCH 24/27] fixed warnings --- src/console_game.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/console_game.rs b/src/console_game.rs index 6c69ff9..774bb07 100644 --- a/src/console_game.rs +++ b/src/console_game.rs @@ -54,7 +54,7 @@ pub fn console_game() { } fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { - let dev_null = |a: usize, b: usize, _c: &Color| -> () { /*println!("Progress: {a}/{b}")*/ }; + let dev_null = |_a: usize, _b: usize, _c: &Color| -> () { /*println!("Progress: {a}/{b}")*/ }; let action = mcts.search(state.clone(), iterations, dev_null); if action.is_ok() { println!("{:?}", action.clone().unwrap().position); From 7f5d9845f6431c6e9394f8ca947b2a819bf3b7c2 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:57:33 +0200 Subject: [PATCH 25/27] changed test module name to snake_case --- src/othello.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/othello.rs b/src/othello.rs index 5583922..8316a5e 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -510,7 +510,7 @@ pub fn print_state(state: State) { } #[cfg(test)] -mod OthelloTests { +mod othello_tests { use super::*; #[test] From 9c58807bdcb02790a96004ed1f2cce7b8d4c22a9 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:59:01 +0200 Subject: [PATCH 26/27] removed commented out code --- src/bin/ai-test.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/bin/ai-test.rs b/src/bin/ai-test.rs index 99dcb43..346f86a 100644 --- a/src/bin/ai-test.rs +++ b/src/bin/ai-test.rs @@ -38,30 +38,6 @@ pub fn main() { }; println!("{win_balance}") } -/* -fn determine_winner(state: State) -> isize { - let p1 = 1; - let p2 = 0; - let mut p1_score: isize = 0; - let mut p2_score: isize = 0; - for row in state.board { - for ch in row { - if ch == p1 { - p1_score += 1; - }else if ch == p2 { - p2_score += 1; - } - } - } - match p1_score - p2_score { - x if x > 0 => 1, - x if x < 0 => -1, - _ => 0, - } - //println!("Score is\t{} {} : {} {}", p1, p1_score, p2_score, p2); - -} -*/ fn ai_turn(mcts: &mut MCTS, state: State, iterations: usize) -> State { let dev_null = |_a: usize, _b: usize, _c: &Color| -> () {}; From df66ac538279ea61347b860d13500fc1898f16f9 Mon Sep 17 00:00:00 2001 From: Philip Cramer <107579314+PhilipCramer@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:26:44 +0200 Subject: [PATCH 27/27] Fixed parse_state to match implementation --- src/othello.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/othello.rs b/src/othello.rs index 152abc5..c442a2d 100644 --- a/src/othello.rs +++ b/src/othello.rs @@ -178,6 +178,10 @@ impl Board { new_rows[(center + 1) as usize] = Row::new(0b0110 << (center * FIELD_SIZE)); Self { rows: new_rows } } + fn blank() -> Board { + let new_rows = [Row::new(0); BOARD_SIZE as usize]; + Self { rows: new_rows } + } fn flip_pieces(&self, action: Action, position: Position, dir: Direction) -> Option { let mut to_flip = Vec::new(); let mut current_pos = position; @@ -451,11 +455,10 @@ pub fn caculate_win(state: State) -> Option { } pub fn parse_state(json: serde_json::Value) -> State { - todo!("Fix parse_state") - /* TODO: Fix - let mut new_board = [[-1; BOARD_SIZE]; BOARD_SIZE]; + //todo!("Fix parse_state") + let mut new_board = Board::blank(); let mut moves_left: u8 = 0; - let _next = match json["turn"] { + let next = match json["turn"] { serde_json::Value::Bool(true) => Color::BLACK, _ => Color::WHITE, }; @@ -464,10 +467,13 @@ pub fn parse_state(json: serde_json::Value) -> State { if let Some(row) = row.as_array() { for (y, cell) in row.iter().enumerate() { match cell.as_i64() { - Some(1) => new_board[x][y] = 1, - Some(0) => new_board[x][y] = 0, + Some(1) => { + new_board.rows[y] = new_board.rows[y].set_pos(Color::WHITE, x).unwrap() + } + Some(0) => { + new_board.rows[y] = new_board.rows[y].set_pos(Color::BLACK, x).unwrap() + } Some(-1) => { - new_board[x][y] = -1; moves_left += 1; } _ => {} @@ -480,7 +486,8 @@ pub fn parse_state(json: serde_json::Value) -> State { board: new_board, next_turn: next, remaining_moves: moves_left, - }*/ + prev_player_skipped: false, + } } pub fn print_state(state: State) {