Skip to content

Commit

Permalink
Merge pull request #31 from Kapsyloffer/Chumbucket
Browse files Browse the repository at this point in the history
Chumbucket AI
  • Loading branch information
Kapsyloffer authored Feb 25, 2024
2 parents bbd4d98 + 0d61cc9 commit fa49f8d
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 32 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
161 changes: 161 additions & 0 deletions src/ai/chum_bucket.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//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<MovementAction>,
best_move_a: Option<MovementAction>,
best_rock_count: i8,
best_range: i64
}

impl ChumBucket {
pub fn new() -> ChumBucket {
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:
//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) {
//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();

//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);

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) {
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 {

//Get each move for this rock
let moves = Tile::get_possible_moves(&home_board, false, passive_pos);

//Get the move deltas
for new_passive_pos in moves {
//Deltas
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 {
//So that we don't mess up the og states.
let mut home_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 + 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);
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 game_clone = game_state.clone();

let mut rock_count: i8 = 0;

//Replace used boards on game_state, then eval
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() {
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());
}

//Opponent colour
let opp_colour = Self::get_opponent(*ai_color);

//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 ||
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_color(),
home_clone.get_home(),
passive_pos.0,
passive_pos.1,
new_passive_pos.0,
new_passive_pos.1,
false,
String::from("ChumBucketAI")
);
let move_a = MovementAction::new(opp_clone.get_color(),
opp_clone.get_home(),
aggr_pos.0,
aggr_pos.1,
new_aggr_pos.0,
new_aggr_pos.1,
true,
String::from("ChumBucketAI")
);

//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);
}
}
}
}
}
}

pub fn get_rock_positions(b: &Board, target: Tile) -> Vec<(i8, i8)> {
let board_state = b.get_state();
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 as i8, x as i8));
}
}
}
return rock_positions;
}

fn get_opponent(color: Tile) -> Tile {
match color {
Tile::Black => return Tile::White,
Tile::White => return Tile::Black,
Tile::Empty => unimplemented!(),
}
}
}
73 changes: 73 additions & 0 deletions src/ai/chum_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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]
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<(i8, i8)> = Vec::new();
target_w.push((3, 0));

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);
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<(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<(i8, i8)> = 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);
}

#[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, 3, 2, 2, 3, true, String::from("ChumBucketAI"));

assert_eq!(best_moves.1, target_move);
}
4 changes: 4 additions & 0 deletions src/ai/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod chum_bucket;

#[cfg(test)]
pub mod chum_tests;
7 changes: 6 additions & 1 deletion src/api/game_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/api/game_packets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion src/api/handle_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } => {
Expand Down
Loading

0 comments on commit fa49f8d

Please sign in to comment.