diff --git a/src/bored/mod.rs b/src/bored/mod.rs index 9e064bc..26209c1 100644 --- a/src/bored/mod.rs +++ b/src/bored/mod.rs @@ -1,6 +1,11 @@ use raylib::prelude::Color; -use crate::gamedata::{Board, Disk}; +use crate::gamedata::{algorithms::minimax_decision, Board, Disk}; +pub const STARTY: i32 = 9; +pub const STARTX: i32 = 7; +const WX: i32 = 14; +const WY: i32 = 14; +const CIRCLEWIDTH: i32 = 56; #[cfg(test)] mod tests; @@ -14,12 +19,32 @@ impl Default for PlayState { fn default() -> Self { Self { circles: vec![], - bottom: vec![], - player_turn: false, + bottom: vec![6, 6, 6, 6, 6, 6, 6], + player_turn: true, board: Board::default(), } } } +impl PlayState { + pub fn play_human(&mut self, column: i32) { + self.board.play(Disk::P1, column as usize); + + self.bottom[column as usize] -= 1; + let (x, y) = get_circle_coords(column, self.bottom[column as usize]); + self.circles.push((x, y, Disk::P1)); + self.player_turn = false; + } + pub fn play_cpu(&mut self, cook: fn(&Board, Disk, &i32) -> Board) { + self.board + .play(Disk::P2, cook(&self.board, Disk::P2, &5).last_move as usize); + let column: i32 = self.board.last_move; + self.bottom[column as usize] -= 1; + let (x, y) = get_circle_coords(column, self.bottom[column as usize]); + self.circles.push((x, y, Disk::P2)); + self.player_turn = false; + self.player_turn = true; + } +} pub struct MenuState { difficulty: i32, p1: (Color, Disk), @@ -38,3 +63,10 @@ pub enum GameState { Play(PlayState), MainMenu(MenuState), } + +fn get_circle_coords(x: i32, y: i32) -> (i32, i32) { + let mut returned: (i32, i32) = (0, 0); + returned.0 = STARTY + (CIRCLEWIDTH * x) + (WX * x); + returned.1 = STARTX + (CIRCLEWIDTH * y) + (WY * y); + returned +} diff --git a/src/gamedata/algorithms.rs b/src/gamedata/algorithms.rs index 258a258..d3132f6 100644 --- a/src/gamedata/algorithms.rs +++ b/src/gamedata/algorithms.rs @@ -36,20 +36,128 @@ fn minimise(board: &Board, disk: &Disk, depth: &i32) -> (Option, i32) { } } } + +pub fn minimax_decision_pruning(board: &Board, disk: Disk, depth: &i32) -> Board { + let (child, _) = maximise_pruning(board, &disk, depth, i32::MIN, i32::MAX); + match child { + Some(state) => state, + None => Board::default(), + } +} +fn maximise_pruning( + board: &Board, + disk: &Disk, + depth: &i32, + alpha: i32, + mut beta: i32, +) -> (Option, i32) { + match board.game_over() || *depth == 0 { + true => (None, get_score(board, *disk)), + false => { + let (mut max_child, mut max_utility): (Option, i32) = (None, i32::MIN); + for child in board.get_children(*disk) { + let (_, utility) = + minimise_pruning(&child, &flip_disk(*disk), &(depth - 1), alpha, beta); + if utility <= alpha { + break; + } + if utility < beta { + beta = utility; + } + + if utility > max_utility { + (max_child, max_utility) = (Some(child), utility) + } + } + (max_child, max_utility) + } + } +} +fn minimise_pruning( + board: &Board, + disk: &Disk, + depth: &i32, + mut alpha: i32, + beta: i32, +) -> (Option, i32) { + match board.game_over() || *depth == 0 { + true => (None, get_score(board, flip_disk(*disk))), + false => { + let (mut min_child, mut min_utility): (Option, i32) = (None, i32::MAX); + for child in board.get_children(*disk) { + let (_, utility) = + maximise_pruning(&child, &flip_disk(*disk), &(depth - 1), alpha, beta); + if utility >= beta { + break; + } + if utility > alpha { + alpha = utility; + } + if utility < min_utility { + (min_child, min_utility) = (Some(child), utility) + } + } + (min_child, min_utility) + } + } +} #[test] fn minimax_test() { let mut board = Board::default(); let mut disk = Disk::P2; let _depth = 5; - let _turn1 = board.play(disk, minimax_decision(&board, disk, &5).last_move); + let _turn1 = board.play(disk, minimax_decision(&board, disk, &5).last_move as usize); + dbg!(&board.columns); disk = flip_disk(disk); - let _turn2 = board.play(disk, minimax_decision(&board, disk, &5).last_move); + let _turn2 = board.play(disk, minimax_decision(&board, disk, &5).last_move as usize); disk = flip_disk(disk); - let _turn3 = board.play(disk, minimax_decision(&board, disk, &5).last_move); + let _turn3 = board.play(disk, minimax_decision(&board, disk, &5).last_move as usize); disk = flip_disk(disk); - let _turn4 = board.play(disk, minimax_decision(&board, disk, &5).last_move); + let _turn4 = board.play(disk, minimax_decision(&board, disk, &5).last_move as usize); disk = flip_disk(disk); - let _turn5 = board.play(disk, minimax_decision(&board, disk, &5).last_move); + let _turn5 = board.play(disk, minimax_decision(&board, disk, &5).last_move as usize); + for column in board.columns.as_rows() { + column + .iter() + .map(|x| { + print!("{:#?},", x); + x + }) + .count(); + println!(); + } + assert!(false); +} +#[test] +fn minimax_pruning_test() { + let mut board = Board::default(); + let mut disk = Disk::P2; + let _depth = 5; + let _turn1 = board.play( + disk, + minimax_decision_pruning(&board, disk, &5).last_move as usize, + ); + dbg!(&board.columns); + disk = flip_disk(disk); + let _turn2 = board.play( + disk, + minimax_decision_pruning(&board, disk, &5).last_move as usize, + ); + disk = flip_disk(disk); + let _turn3 = board.play( + disk, + minimax_decision_pruning(&board, disk, &5).last_move as usize, + ); + disk = flip_disk(disk); + let _turn4 = board.play( + disk, + minimax_decision_pruning(&board, disk, &5).last_move as usize, + ); + disk = flip_disk(disk); + let _turn5 = board.play( + disk, + minimax_decision_pruning(&board, disk, &5).last_move as usize, + ); for column in board.columns.as_rows() { column .iter() diff --git a/src/gamedata/heuristic.rs b/src/gamedata/heuristic.rs index f52d920..58403da 100644 --- a/src/gamedata/heuristic.rs +++ b/src/gamedata/heuristic.rs @@ -6,11 +6,11 @@ use super::{ Board, Disk, }; //multipliers -const POT_STREAK: i32 = 4; //one streak is kind of poopy -const POT_STREAKS: i32 = 6; +const POT_STREAK: i32 = 3; //one streak is kind of poopy +const POT_STREAKS: i32 = 4; const POT_WIN: i32 = 5; // should be nerfed if its just 1 potential win -const POT_WINS: i32 = 8; -const SCORE_DIFF: i32 = 6; +const POT_WINS: i32 = 6; +const SCORE_DIFF: i32 = 40; const MAX_WINS: i32 = 17; pub fn get_score(board: &Board, disk: Disk) -> i32 { @@ -231,7 +231,7 @@ fn streak_test_1() { board.play(Disk::P2, 2); board.play(Disk::P2, 1); let sequences = get_streaks(&board.columns, &Disk::P2); - assert_eq!(18, potential_streaks(&sequences, &Disk::P2)); + assert_eq!(POT_STREAKS * 3, potential_streaks(&sequences, &Disk::P2)); board.play(Disk::P2, 0); let _sequences = get_streaks(&board.columns, &Disk::P2); board.play(Disk::P2, 3); diff --git a/src/gamedata/mod.rs b/src/gamedata/mod.rs index 7efa4a5..9506583 100644 --- a/src/gamedata/mod.rs +++ b/src/gamedata/mod.rs @@ -1,4 +1,4 @@ -mod algorithms; +pub mod algorithms; mod heuristic; mod indices; mod score_checkers; @@ -13,7 +13,7 @@ pub struct Board { p1_score: i32, p2_score: i32, columns: Array2D, - last_move: usize, + pub last_move: i32, } impl Default for Board { @@ -30,10 +30,10 @@ impl Default for Board { } impl Board { - fn getscore(&self) -> (i32, i32) { + pub fn getscore(&self) -> (i32, i32) { (self.p1_score, self.p2_score) } - fn play(&mut self, disk: Disk, col: usize) -> bool { + pub fn play(&mut self, disk: Disk, col: usize) -> bool { let column = &self.columns.as_columns()[col]; let empty = column.iter().filter(|&a| matches!(a, Disk::EMPTY)).count(); // dbg!(empty); @@ -41,7 +41,8 @@ impl Board { match self.columns.set(top, col, disk) { Ok(_) => { self.score_check((top, col)); - self.last_move = col; + self.last_move = col as i32; + //dbg!(self.p1_score, self.p2_score); true } Err(_) => false, @@ -92,7 +93,7 @@ impl Board { None => (), } } - fn game_over(&self) -> bool { + pub fn game_over(&self) -> bool { self.columns .as_row_major() .iter() diff --git a/src/main.rs b/src/main.rs index a2b7e69..d49a8c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ -use oxidised4::{bored::PlayState, gamedata::Disk}; +use oxidised4::{ + bored::PlayState, + gamedata::{algorithms::minimax_decision_pruning, Disk}, +}; use raylib::prelude::*; const NROW: i32 = 6; const NCOL: i32 = 7; @@ -38,17 +41,26 @@ fn main() { rl.set_target_fps(60); while !rl.window_should_close() { let mut d = rl.begin_drawing(&thread); - if d.is_mouse_button_pressed(MouseButton::MOUSE_LEFT_BUTTON) { - if let Some(column) = get_mouse_column(&d, square_widf) { - let coords = get_circle_coords(1, column); - state.circles.push((coords.1, coords.0, Disk::P1)); + match state.player_turn { + true => { + if d.is_mouse_button_pressed(MouseButton::MOUSE_LEFT_BUTTON) { + if let Some(column) = get_mouse_column(&d, square_widf) { + state.play_human(column); + } + } } + false => state.play_cpu(minimax_decision_pruning), } + if state.board.game_over() { + let scores = state.board.getscore(); + println!("Player score: {} \n CPU Score: {}", scores.0, scores.1); + } + for circle in &state.circles { let (x, y, disk) = circle; let color = match disk { Disk::P1 => Color::RED, - Disk::P2 => Color::BLUE, + Disk::P2 => Color::YELLOW, Disk::EMPTY => Color::WHITE, }; d.draw_texture(&circle_texture, *x, *y, color); @@ -59,27 +71,11 @@ fn main() { } //TODO move this to a struct -const STARTY: i32 = 9; -const STARTX: i32 = 7; +pub const STARTY: i32 = 9; +pub const STARTX: i32 = 7; const WX: i32 = 14; const WY: i32 = 14; const CIRCLEWIDTH: i32 = 56; -fn get_circle_coords(x: i32, y: i32) -> (i32, i32) { - let mut returned: (i32, i32) = (STARTY, STARTX); - match x { - 1 => {} - _ => { - returned.0 = STARTY + (CIRCLEWIDTH * (x - 1)) + (WX * (x - 1)); - } - }; - match y { - 1 => {} - _ => { - returned.1 = STARTX + (CIRCLEWIDTH * (y - 1)) + (WY * (y - 1)); - } - }; - returned -} fn get_mouse_column(rl: &RaylibHandle, sw: i32) -> Option { //row,col return let mouse_pos = rl.get_mouse_x(); @@ -88,7 +84,7 @@ fn get_mouse_column(rl: &RaylibHandle, sw: i32) -> Option { dbg!(mouse_pos < sw * (num) - STARTY); if (mouse_pos > sw * (num - 1) + STARTY) && (mouse_pos < sw * (num) - STARTY) { dbg!(num); - return Some(num); + return Some(num - 1); } } None