From 81fbf77c3af009c184443d1b1be6fe85956e93d3 Mon Sep 17 00:00:00 2001 From: Crilluz Date: Sat, 24 Feb 2024 10:29:10 +0100 Subject: [PATCH 1/9] AI stuff writing has begun --- src/ai/chum_bucket.rs | 65 ++++++++++++++++++++++++++++++++++++++++++ src/ai/chum_tests.rs | 51 +++++++++++++++++++++++++++++++++ src/ai/mod.rs | 4 +++ src/lib.rs | 1 + src/rules/game_tile.rs | 8 +++--- 5 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 src/ai/chum_bucket.rs create mode 100644 src/ai/chum_tests.rs create mode 100644 src/ai/mod.rs diff --git a/src/ai/chum_bucket.rs b/src/ai/chum_bucket.rs new file mode 100644 index 0000000..e4967d1 --- /dev/null +++ b/src/ai/chum_bucket.rs @@ -0,0 +1,65 @@ +//Alterantive name: StupidFish + +use crate::{api::move_handling::MovementAction, rules::{game_board::Board, game_instance::Game, game_tile::Tile}}; + +pub struct ChumBucket { + best_move_p: Option, + best_move_a: Option, + best_rock_count: i8, + best_range: i8 +} + +impl ChumBucket { + pub fn new() -> ChumBucket { + return ChumBucket{best_move_p: None, best_move_a: None, best_rock_count: 0, best_range: 0}; + } + + //This AI is stupid and evaluates only based on: + //Freedom of movement (Higher is better) + //& Enemy rocks remaining. (Lower is better) + pub fn eval_move(&mut self, game: &mut Game, ai_color: Tile) -> (&MovementAction, &MovementAction) { + //First we get the opponent colour + let opp_color = Self::get_opponent(ai_color); + + //Get all boards + let home_b = *game.get_board(ai_color, Tile::Black).unwrap(); + let home_w = *game.get_board(ai_color, Tile::White).unwrap(); + let opp_b = *game.get_board(opp_color, Tile::Black).unwrap(); + let opp_w = *game.get_board(opp_color, Tile::White).unwrap(); + + //Get all rock positions. + let rock_pos_home_b = Self::get_rock_positions(&home_b, ai_color); + let rock_pos_home_w = Self::get_rock_positions(&home_w, ai_color); + let rock_pos_opp_b = Self::get_rock_positions(&opp_b, ai_color); + let rock_pos_opp_w = Self::get_rock_positions(&opp_w, ai_color); + + /* + for pos in rock_pos_home_b { + Tile::get_possible_moves(&home_b, false, pos); + } */ + + unimplemented!(); + } + + pub fn get_rock_positions(b: &Board, target: Tile) -> Vec<(usize, usize)> { + let board_state = b.get_state(); + let mut rock_positions: Vec<(usize, usize)> = Vec::new(); + //Go through each tile in the board and see if it's our rock coloures. + for x in 0..=3 { + for y in 0..=3 { + if board_state[y][x] == target { + rock_positions.push((y, x)); + } + } + } + return rock_positions; + } + + fn get_opponent(color: Tile) -> Tile { + match color { + Tile::Black => return Tile::White, + Tile::White => return Tile::Black, + Tile::Empty => unimplemented!(), + } + } +} \ No newline at end of file diff --git a/src/ai/chum_tests.rs b/src/ai/chum_tests.rs new file mode 100644 index 0000000..085c5cc --- /dev/null +++ b/src/ai/chum_tests.rs @@ -0,0 +1,51 @@ +use crate::rules::{game_board::Board, game_tile::Tile}; + +use super::chum_bucket::ChumBucket; + +#[test] +fn test_get_rock_positions_1() { + let state: [[Tile; 4]; 4] = [ + [Tile::Empty, Tile::Empty, Tile::Empty, Tile::Black], + [Tile::Empty, Tile::Empty, Tile::Empty, Tile::Empty], + [Tile::Empty, Tile::Empty, Tile::Empty, Tile::Empty], + [Tile::White, Tile::Empty, Tile::Empty, Tile::Empty], + ]; + let mut board = Board::new_board(Tile::Black, Tile::White); + board.set_state(&state); + + let mut target_w: Vec<(usize, usize)> = Vec::new(); + target_w.push((3, 0)); + + let mut target_b: Vec<(usize, usize)> = Vec::new(); + target_b.push((0, 3)); + + assert_eq!(ChumBucket::get_rock_positions(&board, Tile::White), target_w); + assert_eq!(ChumBucket::get_rock_positions(&board, Tile::Black), target_b); +} + +#[test] +fn test_get_rock_positions_2() { + let state: [[Tile; 4]; 4] = [ + [Tile::White, Tile::White, Tile::White, Tile::White], + [Tile::Empty, Tile::Empty, Tile::Empty, Tile::Empty], + [Tile::Empty, Tile::Empty, Tile::Empty, Tile::Empty], + [Tile::Black, Tile::Black, Tile::Black, Tile::Black], + ]; + let mut board = Board::new_board(Tile::Black, Tile::White); + board.set_state(&state); + + let mut target_w: Vec<(usize, usize)> = Vec::new(); + target_w.push((0, 0)); + target_w.push((0, 1)); + target_w.push((0, 2)); + target_w.push((0, 3)); + + let mut target_b: Vec<(usize, usize)> = Vec::new(); + target_b.push((3, 0)); + target_b.push((3, 1)); + target_b.push((3, 2)); + target_b.push((3, 3)); + + assert_eq!(ChumBucket::get_rock_positions(&board, Tile::White), target_w); + assert_eq!(ChumBucket::get_rock_positions(&board, Tile::Black), target_b); +} \ No newline at end of file diff --git a/src/ai/mod.rs b/src/ai/mod.rs new file mode 100644 index 0000000..0a90363 --- /dev/null +++ b/src/ai/mod.rs @@ -0,0 +1,4 @@ +pub mod chum_bucket; + +#[cfg(test)] +pub mod chum_tests; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8c74cb4..f174488 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ pub mod api; pub mod rules; +pub mod ai; \ No newline at end of file diff --git a/src/rules/game_tile.rs b/src/rules/game_tile.rs index 5701a0e..29a23fb 100644 --- a/src/rules/game_tile.rs +++ b/src/rules/game_tile.rs @@ -202,19 +202,19 @@ impl Tile { } //If the pushed rock is still on the board. - if on_board{ + if on_board { //If we move one step and a rock is there. Move the rock. - if boardstate[end_y][end_x] != Tile::Empty && !stepping{ + if boardstate[end_y][end_x] != Tile::Empty && !stepping { boardstate[rock_y][rock_x] = boardstate[end_y][end_x]; } //Leapfrog - else if boardstate[step_y][step_x] != Tile::Empty && stepping{ + else if boardstate[step_y][step_x] != Tile::Empty && stepping { boardstate[rock_y][rock_x] = boardstate[step_y][step_x]; //Clear the spot behind us. (The D'Lcrantz method) boardstate[step_y][step_x] = Tile::Empty; } //Edge case; diagonal 2 step pushes that are still on the board. - else if boardstate[end_y][end_x] != Tile::Empty && boardstate[step_y][step_x] == Tile::Empty && stepping{ + else if boardstate[end_y][end_x] != Tile::Empty && boardstate[step_y][step_x] == Tile::Empty && stepping { boardstate[rock_y][rock_x] = boardstate[end_y][end_x]; } } From 30e764ab123f6c71c3124cb7f50fc7810f5cc390 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 25 Feb 2024 11:24:27 +0100 Subject: [PATCH 2/9] I must fix the AI code after I've verified that it works Also addressed issue #30 --- src/ai/chum_bucket.rs | 115 +++++++++++++++++++++++++++++++------ src/ai/chum_tests.rs | 8 +-- src/api/move_handling.rs | 38 +++++++----- src/rules/game_instance.rs | 4 ++ 4 files changed, 127 insertions(+), 38 deletions(-) diff --git a/src/ai/chum_bucket.rs b/src/ai/chum_bucket.rs index e4967d1..7a7171e 100644 --- a/src/ai/chum_bucket.rs +++ b/src/ai/chum_bucket.rs @@ -17,38 +17,115 @@ impl ChumBucket { //This AI is stupid and evaluates only based on: //Freedom of movement (Higher is better) //& Enemy rocks remaining. (Lower is better) - pub fn eval_move(&mut self, game: &mut Game, ai_color: Tile) -> (&MovementAction, &MovementAction) { + pub fn get_move(&mut self, game: &mut Game, ai_color: Tile) -> (&MovementAction, &MovementAction) { //First we get the opponent colour let opp_color = Self::get_opponent(ai_color); //Get all boards - let home_b = *game.get_board(ai_color, Tile::Black).unwrap(); - let home_w = *game.get_board(ai_color, Tile::White).unwrap(); - let opp_b = *game.get_board(opp_color, Tile::Black).unwrap(); - let opp_w = *game.get_board(opp_color, Tile::White).unwrap(); - - //Get all rock positions. - let rock_pos_home_b = Self::get_rock_positions(&home_b, ai_color); - let rock_pos_home_w = Self::get_rock_positions(&home_w, ai_color); - let rock_pos_opp_b = Self::get_rock_positions(&opp_b, ai_color); - let rock_pos_opp_w = Self::get_rock_positions(&opp_w, ai_color); - - /* - for pos in rock_pos_home_b { - Tile::get_possible_moves(&home_b, false, pos); - } */ + let mut home_b = *game.get_board(ai_color, Tile::Black).unwrap(); + let mut home_w = *game.get_board(ai_color, Tile::White).unwrap(); + let mut opp_b = *game.get_board(opp_color, Tile::Black).unwrap(); + let mut opp_w = *game.get_board(opp_color, Tile::White).unwrap(); + + //Evaluate for each. + self.eval_move(&home_b, &opp_w, game, &ai_color); + self.eval_move(&home_b, &home_w, game, &ai_color); + self.eval_move(&home_w, &opp_b, game, &ai_color); + self.eval_move(&home_w, &home_b, game, &ai_color); unimplemented!(); } - pub fn get_rock_positions(b: &Board, target: Tile) -> Vec<(usize, usize)> { + pub fn eval_move(&mut self, home_board: &Board, opp_board: &Board, game_state: &Game, ai_color: &Tile) { + let rock_positions_passive = Self::get_rock_positions(&home_board, *ai_color); + let rock_positions_aggressive = Self::get_rock_positions(&opp_board, *ai_color); + + //Runs for each rock on home. + for passive_pos in rock_positions_passive { + + //Get each move for this rock + let moves = Tile::get_possible_moves(&home_board, false, passive_pos); + + //Get the move deltas + for m in moves { + //Deltas + let dy = m.0 - passive_pos.0; + let dx = m.1 - passive_pos.1; + + //Try to make moves + for aggr_pos in &rock_positions_aggressive { + //So that we don't mess up the og states. + let mut home_clone = home_board.clone(); + let mut opp_clone = home_board.clone(); + + //New aggr pos defined using deltas + let new_aggr_pos: (i8, i8) = (aggr_pos.0 + dy, aggr_pos.1 + dx); + + //If both true both moves are valid. + let moved_p = Tile::passive_move(&mut home_clone, passive_pos, m); + let moved_a = Tile::aggressive_move(&mut opp_clone, *aggr_pos, new_aggr_pos); + + //Evaluate. + if moved_p && moved_a { + //Gamestate clone so that we don't mess anything up. + let mut game_clone = game_state.clone(); + + //Fetch each boards + let mut boards = game_clone.get_boards(); + + let mut rock_count:i8 = 0; + let mut range_count:i8 = 0; + //Replace used boards on game_state, then eval + for mut game_board in boards { + //If both home and colour match for home_b + if game_board.get_home() == home_clone.get_home() + && game_board.get_color() == home_clone.get_color() { + game_board.set_state(home_clone.get_state()); + } + + //If both home and colour match for opp_b + if game_board.get_home() == opp_clone.get_home() + && game_board.get_color() == opp_clone.get_color() { + game_board.set_state(opp_clone.get_state()); + } + + let opp_colour = Self::get_opponent(*ai_color); + //Eval range + range_count += Self::get_rock_positions(&game_board, *ai_color).len() as i8; + + //Eval Opponent rocks + rock_count += Self::get_rock_positions(&game_board, opp_colour).len() as i8; + } + + if rock_count < self.best_rock_count && range_count >= self.best_range { + //Obv very good + self.best_rock_count = rock_count; + self.best_range = range_count; + //TODO + /* + board_colour: Tile, + home_colour: Tile, + x1: i8, + y1: i8, + x2: i8, + y2: i8, + aggr: bool, + player: String, */ + } + } + } + } + } + } + + pub fn get_rock_positions(b: &Board, target: Tile) -> Vec<(i8, i8)> { let board_state = b.get_state(); - let mut rock_positions: Vec<(usize, usize)> = Vec::new(); + let mut rock_positions: Vec<(i8, i8)> = Vec::new(); //Go through each tile in the board and see if it's our rock coloures. for x in 0..=3 { for y in 0..=3 { if board_state[y][x] == target { - rock_positions.push((y, x)); + rock_positions.push((y as i8, x as i8)); } } } diff --git a/src/ai/chum_tests.rs b/src/ai/chum_tests.rs index 085c5cc..ab5d36b 100644 --- a/src/ai/chum_tests.rs +++ b/src/ai/chum_tests.rs @@ -13,10 +13,10 @@ fn test_get_rock_positions_1() { let mut board = Board::new_board(Tile::Black, Tile::White); board.set_state(&state); - let mut target_w: Vec<(usize, usize)> = Vec::new(); + let mut target_w: Vec<(i8, i8)> = Vec::new(); target_w.push((3, 0)); - let mut target_b: Vec<(usize, usize)> = Vec::new(); + let mut target_b: Vec<(i8, i8)> = Vec::new(); target_b.push((0, 3)); assert_eq!(ChumBucket::get_rock_positions(&board, Tile::White), target_w); @@ -34,13 +34,13 @@ fn test_get_rock_positions_2() { let mut board = Board::new_board(Tile::Black, Tile::White); board.set_state(&state); - let mut target_w: Vec<(usize, usize)> = Vec::new(); + let mut target_w: Vec<(i8, i8)> = Vec::new(); target_w.push((0, 0)); target_w.push((0, 1)); target_w.push((0, 2)); target_w.push((0, 3)); - let mut target_b: Vec<(usize, usize)> = Vec::new(); + let mut target_b: Vec<(i8, i8)> = Vec::new(); target_b.push((3, 0)); target_b.push((3, 1)); target_b.push((3, 2)); diff --git a/src/api/move_handling.rs b/src/api/move_handling.rs index ef636f4..20402b7 100644 --- a/src/api/move_handling.rs +++ b/src/api/move_handling.rs @@ -107,24 +107,32 @@ pub async fn fetch_moves(socket: &mut WebSocket, game_hodler: &GameHodler, url: let mut move_list = format!("{:?}", Tile::get_possible_moves(b, *aggr, (*x, *y))); - /* We may not fetch moves if: - It's not your turn, - It's not your piece, - It's not your homeboard (passive move), - If the game is over. - If the game is not full.*/ - if game.is_player(player) != game.get_turn() - || b.get_state()[*x as usize][*y as usize] != game.is_player(player) - || !aggr && game.is_player(player) != b.get_home() - || game.has_winner() - || game.get_players().0 == "None" - || game.get_players().1 == "None" { - //println!("Don't cheat, bad things will happen to ya!"); - //return; + //Cannot fetch if it's not your turn. + if game.is_player(player) != game.get_turn() { move_list = format!("[]"); } - + //Cannot fetch if it's not your piece + if b.get_state()[*x as usize][*y as usize] != game.is_player(player) { + move_list = format!("[]"); + } + + //Cannot make a passive move outside of your own homeboards. + if !aggr && game.is_player(player) != b.get_home() { + move_list = format!("[]"); + } + + //Cannot fetch moves if the game is over. + if game.has_winner() { + move_list = format!("[]"); + } + + //Cannot fetch moves if the game has not started. + if game.get_players().0 == "None" || game.get_players().1 == "None" { + move_list = format!("[]"); + } + + //Send it let packet = GamePacket::FetchedMoves { moves: move_list }; if socket .send(Message::Text(serde_json::to_string(&packet).unwrap())) diff --git a/src/rules/game_instance.rs b/src/rules/game_instance.rs index 04299d5..225f9ac 100644 --- a/src/rules/game_instance.rs +++ b/src/rules/game_instance.rs @@ -106,6 +106,10 @@ impl Game { return None; } + pub fn get_boards(&self) -> [Board; 4] { + return self.boards; + } + //Used for "fancy print" in CLI. pub fn display(&mut self) -> String { let mut disp: String = String::from("\n\n\tS H O B U\n\n"); From 7709a1c39b4fa926db58b9514482914960f60e0d Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 25 Feb 2024 11:40:07 +0100 Subject: [PATCH 3/9] I believe it's ready for testing --- src/ai/chum_bucket.rs | 52 ++++++++++++++++++++++++---------------- src/api/move_handling.rs | 15 ++++++++++++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/ai/chum_bucket.rs b/src/ai/chum_bucket.rs index 7a7171e..a1051f1 100644 --- a/src/ai/chum_bucket.rs +++ b/src/ai/chum_bucket.rs @@ -17,15 +17,15 @@ impl ChumBucket { //This AI is stupid and evaluates only based on: //Freedom of movement (Higher is better) //& Enemy rocks remaining. (Lower is better) - pub fn get_move(&mut self, game: &mut Game, ai_color: Tile) -> (&MovementAction, &MovementAction) { + pub fn get_move(&mut self, game: &mut Game, ai_color: Tile) -> (MovementAction, MovementAction) { //First we get the opponent colour let opp_color = Self::get_opponent(ai_color); //Get all boards - let mut home_b = *game.get_board(ai_color, Tile::Black).unwrap(); - let mut home_w = *game.get_board(ai_color, Tile::White).unwrap(); - let mut opp_b = *game.get_board(opp_color, Tile::Black).unwrap(); - let mut opp_w = *game.get_board(opp_color, Tile::White).unwrap(); + let home_b = *game.get_board(ai_color, Tile::Black).unwrap(); + let home_w = *game.get_board(ai_color, Tile::White).unwrap(); + let opp_b = *game.get_board(opp_color, Tile::Black).unwrap(); + let opp_w = *game.get_board(opp_color, Tile::White).unwrap(); //Evaluate for each. self.eval_move(&home_b, &opp_w, game, &ai_color); @@ -33,7 +33,7 @@ impl ChumBucket { self.eval_move(&home_w, &opp_b, game, &ai_color); self.eval_move(&home_w, &home_b, game, &ai_color); - unimplemented!(); + return (self.best_move_p.clone().unwrap(), self.best_move_a.clone().unwrap()); } pub fn eval_move(&mut self, home_board: &Board, opp_board: &Board, game_state: &Game, ai_color: &Tile) { @@ -68,15 +68,13 @@ impl ChumBucket { //Evaluate. if moved_p && moved_a { //Gamestate clone so that we don't mess anything up. - let mut game_clone = game_state.clone(); - - //Fetch each boards - let mut boards = game_clone.get_boards(); + let game_clone = game_state.clone(); let mut rock_count:i8 = 0; let mut range_count:i8 = 0; + //Replace used boards on game_state, then eval - for mut game_board in boards { + for mut game_board in game_clone.get_boards() { //If both home and colour match for home_b if game_board.get_home() == home_clone.get_home() && game_board.get_color() == home_clone.get_color() { @@ -101,16 +99,28 @@ impl ChumBucket { //Obv very good self.best_rock_count = rock_count; self.best_range = range_count; - //TODO - /* - board_colour: Tile, - home_colour: Tile, - x1: i8, - y1: i8, - x2: i8, - y2: i8, - aggr: bool, - player: String, */ + + let move_p = MovementAction::new(home_clone.get_home(), + home_clone.get_color(), + passive_pos.1, + passive_pos.0, + m.1, + m.0, + false, + String::from("ChumBucketAI") + ); + let move_a = MovementAction::new(opp_clone.get_home(), + opp_clone.get_color(), + aggr_pos.1, + aggr_pos.0, + m.1, + m.0, + false, + String::from("ChumBucketAI") + ); + + self.best_move_p = Some(move_p); + self.best_move_a = Some(move_a); } } } diff --git a/src/api/move_handling.rs b/src/api/move_handling.rs index 20402b7..65fec3a 100644 --- a/src/api/move_handling.rs +++ b/src/api/move_handling.rs @@ -15,6 +15,21 @@ pub struct MovementAction { player: String, } +impl MovementAction { + pub fn new(b: Tile, h: Tile, x1: i8, y1: i8, x2: i8, y2: i8, a: bool, p: String) -> MovementAction{ + return MovementAction{ + board_colour: b, + home_colour: h, + x1: x1, + y1: y1, + x2: x2, + y2: y2, + aggr: a, + player: p, + }; + } +} + pub async fn do_move(game_hodler: &GameHodler, url: &String, move_p: &MovementAction, move_a: &MovementAction) { let mut games = game_hodler.games.lock().unwrap(); let Some(game) = games.get_mut(url) else { From 3c9316545d0479e926a26f8f82b950877db011a9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 25 Feb 2024 12:00:45 +0100 Subject: [PATCH 4/9] IT LIVES --- src/ai/chum_bucket.rs | 15 +++++++++------ src/ai/chum_tests.rs | 24 +++++++++++++++++++++++- src/api/move_handling.rs | 2 +- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/ai/chum_bucket.rs b/src/ai/chum_bucket.rs index a1051f1..b0db1b8 100644 --- a/src/ai/chum_bucket.rs +++ b/src/ai/chum_bucket.rs @@ -11,7 +11,7 @@ pub struct ChumBucket { impl ChumBucket { pub fn new() -> ChumBucket { - return ChumBucket{best_move_p: None, best_move_a: None, best_rock_count: 0, best_range: 0}; + return ChumBucket{best_move_p: None, best_move_a: None, best_rock_count: 100, best_range: 0}; } //This AI is stupid and evaluates only based on: @@ -56,7 +56,7 @@ impl ChumBucket { for aggr_pos in &rock_positions_aggressive { //So that we don't mess up the og states. let mut home_clone = home_board.clone(); - let mut opp_clone = home_board.clone(); + let mut opp_clone = opp_board.clone(); //New aggr pos defined using deltas let new_aggr_pos: (i8, i8) = (aggr_pos.0 + dy, aggr_pos.1 + dx); @@ -95,7 +95,7 @@ impl ChumBucket { rock_count += Self::get_rock_positions(&game_board, opp_colour).len() as i8; } - if rock_count < self.best_rock_count && range_count >= self.best_range { + if rock_count <= self.best_rock_count /*&& range_count > self.best_range*/ { //Obv very good self.best_rock_count = rock_count; self.best_range = range_count; @@ -113,12 +113,15 @@ impl ChumBucket { opp_clone.get_color(), aggr_pos.1, aggr_pos.0, - m.1, - m.0, - false, + new_aggr_pos.1, + new_aggr_pos.0, + true, String::from("ChumBucketAI") ); + println!("\nOUR BEST MOVE:\nROCKS:{}\nRANGE:{}\nMOVE_P{:#?}\nMOVE_A:{:#?}", + rock_count, range_count, move_p, move_a); + self.best_move_p = Some(move_p); self.best_move_a = Some(move_a); } diff --git a/src/ai/chum_tests.rs b/src/ai/chum_tests.rs index ab5d36b..ff35b85 100644 --- a/src/ai/chum_tests.rs +++ b/src/ai/chum_tests.rs @@ -1,5 +1,6 @@ use crate::rules::{game_board::Board, game_tile::Tile}; - +use crate::rules::game_instance::Game; +use crate::api::move_handling::MovementAction; use super::chum_bucket::ChumBucket; #[test] @@ -48,4 +49,25 @@ fn test_get_rock_positions_2() { assert_eq!(ChumBucket::get_rock_positions(&board, Tile::White), target_w); assert_eq!(ChumBucket::get_rock_positions(&board, Tile::Black), target_b); +} + +#[test] +fn test_ai_1() { + let state_ww: [[Tile; 4]; 4] = [ + [Tile::Empty, Tile::Empty, Tile::Empty, Tile::Empty], + [Tile::Empty, Tile::Empty, Tile::Empty, Tile::Empty], + [Tile::Empty, Tile::Empty, Tile::Empty, Tile::White], + [Tile::Black, Tile::Black, Tile::Black, Tile::Black], + ]; + + let mut g = Game::new_game(); + g.get_board(Tile::White, Tile::White).unwrap().set_state(&state_ww); + + let mut ai = ChumBucket::new(); + let best_moves = ai.get_move(&mut g, Tile::Black); + println!("\n{:#?}\n", best_moves); + + let target_move = MovementAction::new(Tile::White, Tile::White, 2, 3, 3, 2, true, String::from("ChumBucketAI")); + + assert_eq!(best_moves.1, target_move); } \ No newline at end of file diff --git a/src/api/move_handling.rs b/src/api/move_handling.rs index 65fec3a..9bf82fa 100644 --- a/src/api/move_handling.rs +++ b/src/api/move_handling.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{api::game_packets::*, rules::{game_board::Board, game_hodler::GameHodler, game_tile::Tile}}; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct MovementAction { board_colour: Tile, home_colour: Tile, From f306008d9faba4179c3924647f240f079df9fede Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 25 Feb 2024 14:26:05 +0100 Subject: [PATCH 5/9] HE WORKS! --- src/ai/chum_bucket.rs | 38 +++++++++++++++++++----------------- src/api/move_handling.rs | 42 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/ai/chum_bucket.rs b/src/ai/chum_bucket.rs index b0db1b8..30a7159 100644 --- a/src/ai/chum_bucket.rs +++ b/src/ai/chum_bucket.rs @@ -47,10 +47,10 @@ impl ChumBucket { let moves = Tile::get_possible_moves(&home_board, false, passive_pos); //Get the move deltas - for m in moves { + for new_passive_pos in moves { //Deltas - let dy = m.0 - passive_pos.0; - let dx = m.1 - passive_pos.1; + let dy = new_passive_pos.1 - passive_pos.1; + let dx = new_passive_pos.0 - passive_pos.0; //Try to make moves for aggr_pos in &rock_positions_aggressive { @@ -62,7 +62,7 @@ impl ChumBucket { let new_aggr_pos: (i8, i8) = (aggr_pos.0 + dy, aggr_pos.1 + dx); //If both true both moves are valid. - let moved_p = Tile::passive_move(&mut home_clone, passive_pos, m); + let moved_p = Tile::passive_move(&mut home_clone, passive_pos, new_passive_pos); let moved_a = Tile::aggressive_move(&mut opp_clone, *aggr_pos, new_aggr_pos); //Evaluate. @@ -70,8 +70,8 @@ impl ChumBucket { //Gamestate clone so that we don't mess anything up. let game_clone = game_state.clone(); - let mut rock_count:i8 = 0; - let mut range_count:i8 = 0; + let mut rock_count: i8 = 0; + let mut range_count: i8 = 0; //Replace used boards on game_state, then eval for mut game_board in game_clone.get_boards() { @@ -90,41 +90,43 @@ impl ChumBucket { let opp_colour = Self::get_opponent(*ai_color); //Eval range range_count += Self::get_rock_positions(&game_board, *ai_color).len() as i8; + //TODO: Only for homeboards. //Eval Opponent rocks rock_count += Self::get_rock_positions(&game_board, opp_colour).len() as i8; } - if rock_count <= self.best_rock_count /*&& range_count > self.best_range*/ { + if rock_count < self.best_rock_count + || rock_count == self.best_rock_count && range_count > self.best_range { //Obv very good self.best_rock_count = rock_count; self.best_range = range_count; - let move_p = MovementAction::new(home_clone.get_home(), - home_clone.get_color(), - passive_pos.1, + let move_p = MovementAction::new(home_clone.get_color(), + home_clone.get_home(), passive_pos.0, - m.1, - m.0, + passive_pos.1, + new_passive_pos.0, + new_passive_pos.1, false, String::from("ChumBucketAI") ); - let move_a = MovementAction::new(opp_clone.get_home(), - opp_clone.get_color(), - aggr_pos.1, + let move_a = MovementAction::new(opp_clone.get_color(), + opp_clone.get_home(), aggr_pos.0, - new_aggr_pos.1, + aggr_pos.1, new_aggr_pos.0, + new_aggr_pos.1, true, String::from("ChumBucketAI") ); - println!("\nOUR BEST MOVE:\nROCKS:{}\nRANGE:{}\nMOVE_P{:#?}\nMOVE_A:{:#?}", + println!("\nOUR BEST MOVE:\nROCKS:{}\nRANGE:{}\nMOVE_P:\n{:#?}\nMOVE_A:\n{:#?}", rock_count, range_count, move_p, move_a); self.best_move_p = Some(move_p); self.best_move_a = Some(move_a); - } + } } } } diff --git a/src/api/move_handling.rs b/src/api/move_handling.rs index 9bf82fa..5a7f2c6 100644 --- a/src/api/move_handling.rs +++ b/src/api/move_handling.rs @@ -1,7 +1,7 @@ use axum::extract::ws::{Message, WebSocket}; use serde::{Deserialize, Serialize}; - -use crate::{api::game_packets::*, rules::{game_board::Board, game_hodler::GameHodler, game_tile::Tile}}; +use crate::rules::game_instance::Game; +use crate::{ai::chum_bucket::ChumBucket, api::game_packets::*, rules::{game_board::Board, game_hodler::GameHodler, game_tile::Tile}}; #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct MovementAction { @@ -108,9 +108,45 @@ pub async fn do_move(game_hodler: &GameHodler, url: &String, move_p: &MovementAc //Insert previous move in the game hodler. game_hodler.moves.lock().unwrap().insert(String::from(url), (move_p.clone(), move_a.clone())); game.next_turn(); + + //TODO: Get rid of. + if game.get_players().0 == "ChumBucketAI" && game.get_turn() == Tile::Black { + ai_move(game, Tile::Black); + game.next_turn(); + } + if game.get_players().1 == "ChumBucketAI" && game.get_turn() == Tile::White { + ai_move(game, Tile::White); + game.next_turn(); + } } +} + +//TODO: Get rid of. +fn ai_move(game: &mut Game, ai_color: Tile) { + let mut chummy = ChumBucket::new(); + let (move_p, move_a) = chummy.get_move(game, ai_color); + + //DO MOVE + let board_p: &mut Board = game.get_board(move_p.home_colour, move_p.board_colour).unwrap(); + let move_p = Tile::passive_move(board_p, (move_p.x1, move_p.y1), (move_p.x2, move_p.y2)); - println!("{}", game.display()); + if !move_p { + println!("MOVE_P FAILED!"); + } + + let board_a: &mut Board = game.get_board(move_a.home_colour, move_a.board_colour).unwrap(); + let move_a = Tile::aggressive_move(board_a, (move_a.x1, move_a.y1), (move_a.x2, move_a.y2)); + if !move_a { + println!("MOVE_A FAILED!"); + } + + if move_p && move_a { + println!("AI MOVE: OK!"); + println!("{}", game.display()); + } else { + println!("AI MOVE: NOT OK!"); + println!("{}", game.display()); + } } pub async fn fetch_moves(socket: &mut WebSocket, game_hodler: &GameHodler, url: &String, h: &Tile, c: &Tile, x: &i8, y: &i8, aggr: &bool, player: &String) { From 237bcb6f9ecb5e9d04027dc3373c8af23558894e Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 25 Feb 2024 14:29:58 +0100 Subject: [PATCH 6/9] Test broke idk why --- src/ai/chum_bucket.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ai/chum_bucket.rs b/src/ai/chum_bucket.rs index 30a7159..c74a5a0 100644 --- a/src/ai/chum_bucket.rs +++ b/src/ai/chum_bucket.rs @@ -96,8 +96,7 @@ impl ChumBucket { rock_count += Self::get_rock_positions(&game_board, opp_colour).len() as i8; } - if rock_count < self.best_rock_count - || rock_count == self.best_rock_count && range_count > self.best_range { + if rock_count < self.best_rock_count { //Obv very good self.best_rock_count = rock_count; self.best_range = range_count; From db8384fe0287b81fcdfc82e40bbf3278e9ad2c43 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 25 Feb 2024 14:35:18 +0100 Subject: [PATCH 7/9] Added movement trace to ChumBucketAI --- src/api/move_handling.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/api/move_handling.rs b/src/api/move_handling.rs index 5a7f2c6..de6814a 100644 --- a/src/api/move_handling.rs +++ b/src/api/move_handling.rs @@ -111,42 +111,46 @@ pub async fn do_move(game_hodler: &GameHodler, url: &String, move_p: &MovementAc //TODO: Get rid of. if game.get_players().0 == "ChumBucketAI" && game.get_turn() == Tile::Black { - ai_move(game, Tile::Black); + let (ai_p, ai_a) = ai_move(game, Tile::Black); + game_hodler.moves.lock().unwrap().insert(String::from(url), (ai_p.clone(), ai_a.clone())); game.next_turn(); } if game.get_players().1 == "ChumBucketAI" && game.get_turn() == Tile::White { - ai_move(game, Tile::White); + let (ai_p, ai_a) = ai_move(game, Tile::White); + game_hodler.moves.lock().unwrap().insert(String::from(url), (ai_p.clone(), ai_a.clone())); game.next_turn(); } } } //TODO: Get rid of. -fn ai_move(game: &mut Game, ai_color: Tile) { +fn ai_move(game: &mut Game, ai_color: Tile) -> (MovementAction, MovementAction) { let mut chummy = ChumBucket::new(); let (move_p, move_a) = chummy.get_move(game, ai_color); //DO MOVE let board_p: &mut Board = game.get_board(move_p.home_colour, move_p.board_colour).unwrap(); - let move_p = Tile::passive_move(board_p, (move_p.x1, move_p.y1), (move_p.x2, move_p.y2)); + let moved_p = Tile::passive_move(board_p, (move_p.x1, move_p.y1), (move_p.x2, move_p.y2)); - if !move_p { - println!("MOVE_P FAILED!"); + if !moved_p { + println!("MOVEd_P FAILED!"); } let board_a: &mut Board = game.get_board(move_a.home_colour, move_a.board_colour).unwrap(); - let move_a = Tile::aggressive_move(board_a, (move_a.x1, move_a.y1), (move_a.x2, move_a.y2)); - if !move_a { - println!("MOVE_A FAILED!"); + let moved_a = Tile::aggressive_move(board_a, (move_a.x1, move_a.y1), (move_a.x2, move_a.y2)); + if !moved_a { + println!("MOVEd_A FAILED!"); } - if move_p && move_a { + if moved_p && moved_a { println!("AI MOVE: OK!"); println!("{}", game.display()); } else { println!("AI MOVE: NOT OK!"); println!("{}", game.display()); } + + return (move_p, move_a); } pub async fn fetch_moves(socket: &mut WebSocket, game_hodler: &GameHodler, url: &String, h: &Tile, c: &Tile, x: &i8, y: &i8, aggr: &bool, player: &String) { From e1fa1a6b4ac23bf43ab8de4d4353f44651c6c0a5 Mon Sep 17 00:00:00 2001 From: Crilluz Date: Sun, 25 Feb 2024 16:58:40 +0100 Subject: [PATCH 8/9] Game is now playable vs AI --- README.md | 3 ++- src/ai/chum_bucket.rs | 17 +++++++++++------ src/ai/chum_tests.rs | 2 +- src/api/game_handling.rs | 7 ++++++- src/api/game_packets.rs | 5 +++++ src/api/handle_socket.rs | 5 ++++- src/api/move_handling.rs | 20 +++++++++++++++++--- 7 files changed, 46 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6bf01cb..ab9e8c6 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,8 @@ Standard layout: "DORK" layout: - [ ] Show a little flag next to name. - [ ] Navigatable page. - [ ] Themes for boards and pieces. -- [ ] SHOBU engine/AI. +- [x] SHOBU engine/AI. +- [ ] SHOBU engine/AI but good. - [ ] Movement animations / Smooth movement. - [ ] Allow aggressive moves first / Detect which of the two moves is aggressive. diff --git a/src/ai/chum_bucket.rs b/src/ai/chum_bucket.rs index c74a5a0..bf33f01 100644 --- a/src/ai/chum_bucket.rs +++ b/src/ai/chum_bucket.rs @@ -6,7 +6,7 @@ pub struct ChumBucket { best_move_p: Option, best_move_a: Option, best_rock_count: i8, - best_range: i8 + best_range: i64 } impl ChumBucket { @@ -39,6 +39,7 @@ impl ChumBucket { pub fn eval_move(&mut self, home_board: &Board, opp_board: &Board, game_state: &Game, ai_color: &Tile) { let rock_positions_passive = Self::get_rock_positions(&home_board, *ai_color); let rock_positions_aggressive = Self::get_rock_positions(&opp_board, *ai_color); + let mut range_count: i64 = 0; //Not working rn. //Runs for each rock on home. for passive_pos in rock_positions_passive { @@ -59,7 +60,7 @@ impl ChumBucket { let mut opp_clone = opp_board.clone(); //New aggr pos defined using deltas - let new_aggr_pos: (i8, i8) = (aggr_pos.0 + dy, aggr_pos.1 + dx); + let new_aggr_pos: (i8, i8) = (aggr_pos.0 + dx, aggr_pos.1 + dy); //If both true both moves are valid. let moved_p = Tile::passive_move(&mut home_clone, passive_pos, new_passive_pos); @@ -71,7 +72,6 @@ impl ChumBucket { let game_clone = game_state.clone(); let mut rock_count: i8 = 0; - let mut range_count: i8 = 0; //Replace used boards on game_state, then eval for mut game_board in game_clone.get_boards() { @@ -87,16 +87,21 @@ impl ChumBucket { game_board.set_state(opp_clone.get_state()); } + //Opponent colour let opp_colour = Self::get_opponent(*ai_color); - //Eval range - range_count += Self::get_rock_positions(&game_board, *ai_color).len() as i8; + + //Eval range, but only for homeboards. + if game_board.get_home() == *ai_color { + range_count += Self::get_rock_positions(&game_board, *ai_color).len() as i64; + } //TODO: Only for homeboards. //Eval Opponent rocks rock_count += Self::get_rock_positions(&game_board, opp_colour).len() as i8; } - if rock_count < self.best_rock_count { + if rock_count < self.best_rock_count || + rock_count <= self.best_rock_count && range_count > self.best_range { //Obv very good self.best_rock_count = rock_count; self.best_range = range_count; diff --git a/src/ai/chum_tests.rs b/src/ai/chum_tests.rs index ff35b85..a97205e 100644 --- a/src/ai/chum_tests.rs +++ b/src/ai/chum_tests.rs @@ -67,7 +67,7 @@ fn test_ai_1() { let best_moves = ai.get_move(&mut g, Tile::Black); println!("\n{:#?}\n", best_moves); - let target_move = MovementAction::new(Tile::White, Tile::White, 2, 3, 3, 2, true, String::from("ChumBucketAI")); + let target_move = MovementAction::new(Tile::White, Tile::White, 3, 2, 2, 3, true, String::from("ChumBucketAI")); assert_eq!(best_moves.1, target_move); } \ No newline at end of file diff --git a/src/api/game_handling.rs b/src/api/game_handling.rs index e20b17d..649de04 100644 --- a/src/api/game_handling.rs +++ b/src/api/game_handling.rs @@ -22,7 +22,7 @@ pub async fn fetch_game(socket: &mut WebSocket, url: &String, game_hodler: &Game } } -pub async fn create_game(socket: &mut WebSocket, player_id: String, color: &Tile, game_hodler: &GameHodler) { +pub async fn create_game(socket: &mut WebSocket, player_id: String, color: &Tile, ai: bool, game_hodler: &GameHodler) { //Just to prevent collissions (Rare af but yaknow, just in case.) let map_size = game_hodler.games.lock().unwrap().len(); let url = format!("{}{}", Game::generate_url(), map_size); @@ -34,6 +34,11 @@ pub async fn create_game(socket: &mut WebSocket, player_id: String, color: &Tile .insert(url.to_owned(), Game::new_game()); game_hodler.games.lock().unwrap().get_mut(&url).unwrap().add_player(player_id, Some(*color)); + + //Add chumbucket if we play with AI + if ai { + game_hodler.games.lock().unwrap().get_mut(&url).unwrap().add_player(String::from("ChumBucketAI"), None); + } let packet = GamePacket::GameCreated { url }; if socket diff --git a/src/api/game_packets.rs b/src/api/game_packets.rs index ce2233f..ed7ed28 100644 --- a/src/api/game_packets.rs +++ b/src/api/game_packets.rs @@ -16,6 +16,11 @@ pub (crate) enum GamePacket { player_id: String, color: Tile, }, + //Call to create new game vs ChumBucket. + CreateGameWithAI { + player_id: String, + color: Tile, + }, //Call to fetch game state FetchGame { url: String, diff --git a/src/api/handle_socket.rs b/src/api/handle_socket.rs index 21828ff..c44d13f 100644 --- a/src/api/handle_socket.rs +++ b/src/api/handle_socket.rs @@ -42,7 +42,10 @@ pub async fn handle_socket(mut socket: WebSocket, game_hodler: GameHodler) { } //Create a new game. GamePacket::CreateGame {player_id, color} => { - create_game(&mut socket, player_id, &color, &game_hodler).await; + create_game(&mut socket, player_id, &color, false, &game_hodler).await; + } + GamePacket::CreateGameWithAI { player_id, color } => { + create_game(&mut socket, player_id, &color, true, &game_hodler).await; } //Response on create a new game. GamePacket::GameCreated { url } => { diff --git a/src/api/move_handling.rs b/src/api/move_handling.rs index de6814a..f1b03f2 100644 --- a/src/api/move_handling.rs +++ b/src/api/move_handling.rs @@ -109,17 +109,31 @@ pub async fn do_move(game_hodler: &GameHodler, url: &String, move_p: &MovementAc game_hodler.moves.lock().unwrap().insert(String::from(url), (move_p.clone(), move_a.clone())); game.next_turn(); + //AI CODE //TODO: Get rid of. if game.get_players().0 == "ChumBucketAI" && game.get_turn() == Tile::Black { - let (ai_p, ai_a) = ai_move(game, Tile::Black); + let (ai_p, ai_a) = ai_move(game, Tile::White); game_hodler.moves.lock().unwrap().insert(String::from(url), (ai_p.clone(), ai_a.clone())); - game.next_turn(); + let board_ai_a = game.get_board(ai_a.home_colour, ai_p.board_colour).unwrap(); + let winner = Board::check_winner(&board_ai_a); + game.set_winner(&winner); + + if winner != Tile::Empty { + println!("Winner for game {}: {:?}", url, winner); + } } if game.get_players().1 == "ChumBucketAI" && game.get_turn() == Tile::White { let (ai_p, ai_a) = ai_move(game, Tile::White); game_hodler.moves.lock().unwrap().insert(String::from(url), (ai_p.clone(), ai_a.clone())); - game.next_turn(); + let board_ai_a = game.get_board(ai_a.home_colour, ai_p.board_colour).unwrap(); + let winner = Board::check_winner(&board_ai_a); + game.set_winner(&winner); + + if winner != Tile::Empty { + println!("Winner for game {}: {:?}", url, winner); + } } + game.next_turn(); } } From 0d61cc94e6bc9ef03aef621a321e834ce7c706fb Mon Sep 17 00:00:00 2001 From: Crilluz Date: Sun, 25 Feb 2024 17:07:49 +0100 Subject: [PATCH 9/9] Win/Lose screen if AI wins. --- src/ai/chum_bucket.rs | 4 ++-- src/api/move_handling.rs | 24 +++++++----------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/ai/chum_bucket.rs b/src/ai/chum_bucket.rs index bf33f01..9286d85 100644 --- a/src/ai/chum_bucket.rs +++ b/src/ai/chum_bucket.rs @@ -125,8 +125,8 @@ impl ChumBucket { String::from("ChumBucketAI") ); - println!("\nOUR BEST MOVE:\nROCKS:{}\nRANGE:{}\nMOVE_P:\n{:#?}\nMOVE_A:\n{:#?}", - rock_count, range_count, move_p, move_a); + //println!("\nOUR BEST MOVE:\nROCKS:{}\nRANGE:{}\nMOVE_P:\n{:#?}\nMOVE_A:\n{:#?}", + //rock_count, range_count, move_p, move_a); self.best_move_p = Some(move_p); self.best_move_a = Some(move_a); diff --git a/src/api/move_handling.rs b/src/api/move_handling.rs index f1b03f2..c5a53a1 100644 --- a/src/api/move_handling.rs +++ b/src/api/move_handling.rs @@ -99,12 +99,6 @@ pub async fn do_move(game_hodler: &GameHodler, url: &String, move_p: &MovementAc .unwrap() .set_state(b4_a.get_state()); } else { - let winner = Board::check_winner(&board_a); - game.set_winner(&winner); - - if winner != Tile::Empty { - println!("Winner for game {}: {:?}", url, winner); - } //Insert previous move in the game hodler. game_hodler.moves.lock().unwrap().insert(String::from(url), (move_p.clone(), move_a.clone())); game.next_turn(); @@ -114,25 +108,21 @@ pub async fn do_move(game_hodler: &GameHodler, url: &String, move_p: &MovementAc if game.get_players().0 == "ChumBucketAI" && game.get_turn() == Tile::Black { let (ai_p, ai_a) = ai_move(game, Tile::White); game_hodler.moves.lock().unwrap().insert(String::from(url), (ai_p.clone(), ai_a.clone())); - let board_ai_a = game.get_board(ai_a.home_colour, ai_p.board_colour).unwrap(); - let winner = Board::check_winner(&board_ai_a); - game.set_winner(&winner); - - if winner != Tile::Empty { - println!("Winner for game {}: {:?}", url, winner); - } } if game.get_players().1 == "ChumBucketAI" && game.get_turn() == Tile::White { let (ai_p, ai_a) = ai_move(game, Tile::White); game_hodler.moves.lock().unwrap().insert(String::from(url), (ai_p.clone(), ai_a.clone())); - let board_ai_a = game.get_board(ai_a.home_colour, ai_p.board_colour).unwrap(); - let winner = Board::check_winner(&board_ai_a); - game.set_winner(&winner); - + } + + for board in game.get_boards() { + let winner = Board::check_winner(&board); if winner != Tile::Empty { + game.set_winner(&winner); println!("Winner for game {}: {:?}", url, winner); + break; } } + game.next_turn(); } }