diff --git a/src/bin/gui.rs b/src/bin/gui.rs index 80d70eb..1791db2 100644 --- a/src/bin/gui.rs +++ b/src/bin/gui.rs @@ -25,8 +25,7 @@ struct HoverStack { struct BattleSheepApp { board: Board, hover_stack: Option, - min_home_stack: Option, - max_home_stack: Option, + home_stacks: [Option; Player::PLAYER_COUNT as usize], red_image: RetainedImage, blue_image: RetainedImage, } @@ -39,8 +38,11 @@ impl BattleSheepApp { row_length: 1, }, hover_stack: None, - min_home_stack: Some(Tile::stack(Player::Min, 16)), - max_home_stack: Some(Tile::stack(Player::Max, 16)), + home_stacks: Player::iter() + .map(|player| Some(Tile::stack(player, 16))) + .collect::>() + .try_into() + .unwrap(), red_image: RetainedImage::from_image_bytes( "redsheep.png", include_bytes!("redsheep.png"), @@ -85,8 +87,9 @@ impl BattleSheepApp { stack_size: u8, ) { let image = match player { - Player::Min => &self.red_image, - Player::Max => &self.blue_image, + Player(0) => &self.red_image, + Player(1) => &self.blue_image, + _ => unreachable!(), }; painter.image( image.texture_id(ctx), @@ -156,89 +159,67 @@ impl eframe::App for BattleSheepApp { } } - let max_home = canvas.rect.center_bottom() + vec2(-0.5 * height, -0.5 * height); - if let Some(max_home_stack) = self.max_home_stack { - self.draw_stack( - ctx, - &painter, - max_home, - height, - max_home_stack.player(), - max_home_stack.stack_size(), - ); - } - let min_home = canvas.rect.center_bottom() + vec2(0.5 * height, -0.5 * height); - if let Some(min_home_stack) = self.min_home_stack { - self.draw_stack( - ctx, - &painter, - min_home, - height, - min_home_stack.player(), - min_home_stack.stack_size(), - ); - } + for player in Player::iter() { + let player_id = player.id() as usize; + let home_stack = self.home_stacks[player_id]; - if let Some(pointer_pos) = canvas.hover_pos() { - let pointer_coords = point_to_hex(pointer_pos, grid_start, height); - ui.label(format!("{:?}", pointer_coords)); + let home = canvas.rect.center_bottom() + + vec2( + ((Player::PLAYER_COUNT - 1) as f32 * -0.5 + player_id as f32) * height, + -0.5 * height, + ); + if let Some(home_stack) = home_stack { + self.draw_stack( + ctx, + &painter, + home, + height, + home_stack.player(), + home_stack.stack_size(), + ); + } - /* Did click end on this frame? drag_released() is much like clicked() but without - * time or movement limit. */ - if canvas.drag_released() { - if Rect::from_center_size(max_home, vec2(height, height)).contains(pointer_pos) - { - match self.max_home_stack { - Some(max_home_stack) => { - if let None = self.hover_stack { - self.hover_stack = Some(HoverStack { - stack: max_home_stack, - origin: None, - }); - self.max_home_stack = None; - } - } - None => { - if let Some(HoverStack { - stack: hover_stack, - origin: hover_origin, - }) = self.hover_stack - { - if hover_origin == None { - self.max_home_stack = Some(hover_stack); - self.hover_stack = None; + if let Some(pointer_pos) = canvas.hover_pos() { + /* Did click end on this frame? drag_released() is much like clicked() but without + * time or movement limit. */ + if canvas.drag_released() { + if Rect::from_center_size(home, vec2(height, height)).contains(pointer_pos) + { + match home_stack { + Some(home_stack) => { + if let None = self.hover_stack { + self.hover_stack = Some(HoverStack { + stack: home_stack, + origin: None, + }); + self.home_stacks[player_id] = None; } } - } - } - } else if Rect::from_center_size(min_home, vec2(height, height)) - .contains(pointer_pos) - { - match self.min_home_stack { - Some(min_home_stack) => { - if let None = self.hover_stack { - self.hover_stack = Some(HoverStack { - stack: min_home_stack, - origin: None, - }); - self.min_home_stack = None; - } - } - None => { - if let Some(HoverStack { - stack: hover_stack, - origin: hover_origin, - }) = self.hover_stack - { - if hover_origin == None { - self.min_home_stack = Some(hover_stack); - self.hover_stack = None; + None => { + if let Some(HoverStack { + stack: hover_stack, + origin: hover_origin, + }) = self.hover_stack + { + if hover_origin == None { + self.home_stacks[player_id] = Some(hover_stack); + self.hover_stack = None; + } } } } } } + } + } + if let Some(pointer_pos) = canvas.hover_pos() { + let pointer_coords = point_to_hex(pointer_pos, grid_start, height); + ui.label(format!("{:?}", pointer_coords)); + + /* Did click end on this frame? drag_released() is much like clicked() but without + * time or movement limit. */ + if canvas.drag_released() { let mut clicked_coords = pointer_coords; let clicked_tile = self.board[clicked_coords]; match clicked_tile.tile_type() { diff --git a/src/board.rs b/src/board.rs index fb95845..92d5103 100644 --- a/src/board.rs +++ b/src/board.rs @@ -7,24 +7,31 @@ use std::{ }; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum Player { - Min, - Max, -} +pub struct Player(pub u8); impl Player { - pub fn sign(self) -> i32 { - match self { - Self::Min => -1, - Self::Max => 1, - } + pub const PLAYER_COUNT: u8 = 2; + + pub fn iter() -> impl Iterator { + return (0..Self::PLAYER_COUNT).map(|id| Player(id)); } - pub fn opposite(self) -> Player { - match self { - Player::Min => Player::Max, - Player::Max => Player::Min, - } + pub const fn id(self) -> u8 { + return self.0; + } + + /* The direction where this player is trying to push the game value. */ + pub fn direction(self) -> i32 { + return match self.0 { + 0 => -1, + 1 => 1, + _ => unreachable!(), + }; + } + + /* The player whose turn is next. */ + pub fn next(self) -> Player { + return Player((self.0 + 1) % Self::PLAYER_COUNT); } } @@ -38,11 +45,11 @@ pub enum TileType { /* Custom bitfield struct for saving a Battle Sheep tile into a single byte. * Structure: * 2 bits tile_type, 00 = Stack, 01 = NoTile, 10 or 11 = Empty - * 1 bits player, 0 = Min, 1 = Max + * 1 bits player * 5 bits stack_size, offset by -1 * Numerically: - * 0-31 = Min player's Stack with size 1-32 - * 32-63 = Max player's Stack with size 1-32 + * 0-31 = Player 0 Stack with size 1-32 + * 32-63 = Player 1 Stack with size 1-32 * 64-127 = NoTile * 128-255 = Empty */ #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -51,15 +58,12 @@ pub struct Tile(pub u8); impl Tile { pub const MAX_STACK_SIZE: u8 = 32; - pub const NO_TILE: Self = Self::new(TileType::NoTile, Player::Min, 1); - pub const EMPTY: Self = Self::new(TileType::Empty, Player::Min, 1); + pub const NO_TILE: Self = Self::new(TileType::NoTile, Player(0), 1); + pub const EMPTY: Self = Self::new(TileType::Empty, Player(0), 1); pub const fn new(tile_type: TileType, player: Player, stack_size: u8) -> Self { let bitfield = stack_size - 1 - + match player { - Player::Min => 0, - Player::Max => 32, - } + + player.id() * 32 + match tile_type { TileType::Stack => 0, TileType::NoTile => 64, @@ -83,11 +87,7 @@ impl Tile { } pub fn player(self) -> Player { - if self.0 < 32 { - return Player::Min; - } else { - return Player::Max; - } + return Player(self.0 / 32); } pub fn stack_size(self) -> u8 { @@ -371,8 +371,8 @@ impl Board { tiles.push(Tile::EMPTY); } else { let player = match &tile_content[..1] { - "-" => Player::Min, - "+" => Player::Max, + "-" => Player(0), + "+" => Player(1), _ => return Err("Invalid tile")?, }; @@ -423,8 +423,9 @@ impl Board { } TileType::Stack => { let (symbol, color) = match tile.player() { - Player::Min => ("-", RED), - Player::Max => ("+", BLUE), + Player(0) => ("-", RED), + Player(1) => ("+", BLUE), + _ => unreachable!(), }; if colored { format!("{}{}{:<3}{}", color, symbol, tile.stack_size(), RESET) @@ -504,25 +505,29 @@ impl Board { }); } - /* Evaluates the current board state. Positive number means Max has an advantage, negative means - * Min has it. This is a very simple evaluation function that checks how blocked the stacks are - * by their neighbors and how evenly split they are. In the endgame, another heuristic is used. */ + /* Evaluates the current board state. The more the value is in one player's direction, the more + * advantage they have. This is a very simple evaluation function that checks how blocked the + * stacks are by their neighbors and how evenly split they are. In the endgame, another + * heuristic is used. */ pub fn heuristic_evaluate(&self) -> i32 { let mut value = 0; - let mut max_all_blocked = true; - let mut min_all_blocked = true; - let mut max_stacks = 0; - let mut min_stacks = 0; + let mut player_all_blocked = [true; Player::PLAYER_COUNT as usize]; + let mut player_stacks = [0; Player::PLAYER_COUNT as usize]; - let mut max_largest_stack = 0; - let mut max_smallest_stack = i32::MAX; - let mut min_largest_stack = 0; - let mut min_smallest_stack = i32::MAX; + let mut player_smallest_stack = [i32::MAX; Player::PLAYER_COUNT as usize]; + let mut player_largest_stack = [0; Player::PLAYER_COUNT as usize]; for (coords, tile) in self.iter_row_major() { if tile.is_stack() { let player = tile.player(); let size = tile.stack_size(); + let player_id = player.id() as usize; + + player_stacks[player_id] += 1; + player_largest_stack[player_id] = + i32::max(player_largest_stack[player_id], size as i32); + player_smallest_stack[player_id] = + i32::min(player_smallest_stack[player_id], size as i32); /* A maximum of 6 directions are blocked. */ let mut blocked_directions = 6; @@ -533,75 +538,63 @@ impl Board { } if size > 1 && blocked_directions < 6 { - match player { - Player::Min => min_all_blocked = false, - Player::Max => max_all_blocked = false, - } + player_all_blocked[player_id] = false; } /* Being surrounded from more sides and having more sheep in the stack increase * its blocked score. */ let blocked_score = (size as i32 - 1) * blocked_directions; - match player { - Player::Min => { - /* A blocked Min stack gives an advantage to Max and therefore increases the - * value of the board. Vice versa for Max. */ - value += blocked_score; - min_stacks += 1; - min_largest_stack = i32::max(min_largest_stack, size as i32); - min_smallest_stack = i32::min(min_smallest_stack, size as i32); - } - Player::Max => { - value -= blocked_score; - max_stacks += 1; - max_largest_stack = i32::max(max_largest_stack, size as i32); - max_smallest_stack = i32::min(max_smallest_stack, size as i32); - } - } + /* A blocked stack gives a disadvantage to the player, so the board value is moved + * away from the player's direction. */ + value -= blocked_score * player.direction(); } } /* Extra score for splitting stacks evenly. This does not matter as much as being blocked, * the maximum splitting bonus is 7. */ - value += (min_largest_stack - min_smallest_stack) / 2; - value -= (max_largest_stack - max_smallest_stack) / 2; + for player in Player::iter() { + let player_id = player.id() as usize; + let uneven_score = + (player_largest_stack[player_id] - player_smallest_stack[player_id]) / 2; + value -= uneven_score * player.direction(); + } - /* If at least on player is blocked, use end game evaluation instead. + /* If at least one player is blocked, use end game evaluation instead. * * Both players are blocked, so the game is over and the winner can be determined. */ - if min_all_blocked && max_all_blocked { - if max_stacks > min_stacks { + if player_all_blocked[0] && player_all_blocked[1] { + if player_stacks[1] > player_stacks[0] { value = 1000000; - } else if min_stacks > max_stacks { + } else if player_stacks[0] > player_stacks[1] { value = -1000000; } else { value = match self.largest_connected_field_holder() { - Some(Player::Max) => 1000000, - Some(Player::Min) => -1000000, - None => 0, + Some(Player(1)) => 1000000, + Some(Player(0)) => -1000000, + _ => 0, } } /* Only one player is blocked. In most cases this means that the blocked player has lost. In * the rare case that the beginning player has blocked themselves, there is a chance that * they might still win. */ - } else if min_all_blocked { - if max_stacks >= min_stacks { + } else if player_all_blocked[0] { + if player_stacks[1] >= player_stacks[0] { value = 1000000; } else { /* The rare case where the blocked player might still win. However, if the other * player already has a larger connected field, the blocked player will lose. If * not, the game is not yet over and we fall back to the normal heuristic * evaluation. */ - if let Some(Player::Max) = self.largest_connected_field_holder() { + if let Some(Player(1)) = self.largest_connected_field_holder() { value = 1000000; } } - } else if max_all_blocked { - if min_stacks >= max_stacks { + } else if player_all_blocked[1] { + if player_stacks[0] >= player_stacks[1] { value = -1000000; } else { - if let Some(Player::Min) = self.largest_connected_field_holder() { + if let Some(Player(0)) = self.largest_connected_field_holder() { value = -1000000; } } @@ -612,8 +605,7 @@ impl Board { /* Tells which player has the largest connected field. */ pub fn largest_connected_field_holder(&self) -> Option { - let mut min_largest_field = 0; - let mut max_largest_field = 0; + let mut player_largest_field = [0; Player::PLAYER_COUNT as usize]; let mut visited = vec![false; self.tiles.len()]; let mut dfs_stack = Vec::<(isize, isize)>::new(); @@ -621,6 +613,7 @@ impl Board { for (start_coords, tile) in self.iter_row_major() { if tile.is_stack() && !visited[self.coords_to_index(start_coords)] { let player = tile.player(); + let player_id = player.id() as usize; let mut field_size = 0; /* Depth-first search for counting the size of a connected field. */ @@ -640,20 +633,15 @@ impl Board { } } - match player { - Player::Min => { - min_largest_field = u32::max(min_largest_field, field_size); - } - Player::Max => { - max_largest_field = u32::max(max_largest_field, field_size); - } - } + player_largest_field[player_id] = + u32::max(player_largest_field[player_id], field_size); } } - return if min_largest_field > max_largest_field { - Some(Player::Min) - } else if max_largest_field > min_largest_field { - Some(Player::Max) + + return if player_largest_field[0] > player_largest_field[1] { + Some(Player(0)) + } else if player_largest_field[1] > player_largest_field[0] { + Some(Player(1)) } else { None }; diff --git a/src/lib.rs b/src/lib.rs index 85b45c4..387e061 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ pub fn choose_move( * sooner. * Min's moves are sorted smallest heuristic first and Max's by largest first. */ let mut moves = sort_iter_by_cached_key(board.possible_moves(player), |next_board| { - -player.sign() * next_board.heuristic_evaluate() + -player.direction() * next_board.heuristic_evaluate() }); /* Result is wrapped in a mutex so it can be updated from multiple threads. */ @@ -53,7 +53,7 @@ pub fn choose_move( * bounds and the resulting value are negated. This allows us to use the same function for * both players. */ let (val, visited) = evaluate( - player.opposite(), + player.next(), &next_board, heuristic_depth - 1, -beta, @@ -96,7 +96,7 @@ pub fn choose_move( /* If there were no possible moves, fall back to heuristic evaluation. */ if max_value == i32::MIN { let chosen_move = None; - let max_value = player.sign() * board.heuristic_evaluate(); + let max_value = player.direction() * board.heuristic_evaluate(); let total_visited = 1; return (chosen_move, max_value, total_visited); } @@ -114,7 +114,7 @@ pub fn evaluate( ) -> (i32, u64) { /* At depth 0 use heuristic evaluation. */ if heuristic_depth == 0 { - let max_value = player.sign() * board.heuristic_evaluate(); + let max_value = player.direction() * board.heuristic_evaluate(); let total_visited = 1; return (max_value, total_visited); } else { @@ -127,7 +127,7 @@ pub fn evaluate( * pruning to take effect sooner. * Min's moves are sorted smallest heuristic first and Max's by largest first. */ let moves = sort_iter_by_cached_key(board.possible_moves(player), |next_board| { - -player.sign() * next_board.heuristic_evaluate() + -player.direction() * next_board.heuristic_evaluate() }); result = minimax_evaluate(player, moves, heuristic_depth, alpha, beta); } else { @@ -140,7 +140,7 @@ pub fn evaluate( /* If there were no possible moves, fall back to heuristic evaluation. */ if max_value == i32::MIN { - let max_value = player.sign() * board.heuristic_evaluate(); + let max_value = player.direction() * board.heuristic_evaluate(); let total_visited = 1; return (max_value, total_visited); } @@ -169,7 +169,7 @@ pub fn minimax_evaluate>( * bounds and the resulting value are negated. This allows us to use the same function for * both players. */ let (val, visited) = evaluate( - player.opposite(), + player.next(), &next_board, heuristic_depth - 1, -beta, diff --git a/src/main.rs b/src/main.rs index 657ae19..1fe2e76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ fn main() { args[0] ); } - let human_max = match args[1].as_str() { + let human_player = match args[1].as_str() { "-p" => true, "-w" => false, _ => unreachable!(), @@ -27,8 +27,8 @@ fn main() { let mut board = read_board_from_user(); println!("{}", board.write(true)); - /* Min always starts. */ - let mut player = Player::Min; + /* Player 0 always starts. */ + let mut player = Player(0); let mut turns = 0; let mut total_duration = Duration::ZERO; @@ -39,16 +39,16 @@ fn main() { /* The player chooses a move. */ let (next_board, val, visited) = choose_move(player, &board, 7, i32::MIN + 1, i32::MAX); - let value = player.sign() * val; + let value = player.direction() * val; match next_board { None => { /* The player could not choose a move, so the game is over. */ println!(); if value > 0 { - println!("Max won!"); + println!("Blue won!"); } else if value < 0 { - println!("Min won!") + println!("Red won!") } else { println!("Draw!") } @@ -66,8 +66,9 @@ fn main() { println!( "{}'s turn", match player { - Player::Min => "Min", - Player::Max => "Max", + Player(0) => "Red", + Player(1) => "Blue", + _ => unreachable!(), } ); println!( @@ -80,23 +81,20 @@ fn main() { turns += 1; /* Setting up the next turn. */ - if human_max { - /* Max is a human player (the user). Their whole turn is played just by asking - * them for a board. After that it's Min's turn again. */ + if human_player { + /* Player 1 is a human player (the user). Their whole turn is played just by asking + * them for a board. After that it's Player 0's turn again. */ println!(); - println!("Max's turn"); + println!("Blue's turn"); println!("Enter a board (finish with an empty line)"); board = read_board_from_user(); println!("{}", board.write(true)); - player = Player::Min; + player = Player(0); } else { - /* The next turn is played by the opposite player. */ + /* The next turn is played by another player. */ board = next_board; - player = match player { - Player::Min => Player::Max, - Player::Max => Player::Min, - }; + player = player.next(); } } } diff --git a/src/tests.rs b/src/tests.rs index e98e89a..a33e242 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -61,7 +61,7 @@ fn possible_moves_are_found() { assert_eq!( Board::parse(input) .unwrap() - .possible_moves(Player::Max) + .possible_moves(Player(1)) .collect::>(), max_moves .iter() @@ -113,7 +113,7 @@ fn possible_starting_moves_are_found() { assert_eq!( Board::parse(input) .unwrap() - .possible_moves(Player::Min) + .possible_moves(Player(0)) .collect::>(), min_moves .iter() @@ -243,13 +243,13 @@ fn ai_chooses_only_option_and_loses() { " .trim_matches('\n'); let (next_board, val, visited) = choose_move( - Player::Max, + Player(1), &Board::parse(max_can_move).unwrap(), 2, i32::MIN + 1, i32::MAX, ); - let value = Player::Max.sign() * val; + let value = Player(1).direction() * val; assert_eq!(next_board, Some(Board::parse(max_moved).unwrap())); assert_eq!(value, -1000000); assert!(visited > 0); @@ -274,13 +274,13 @@ fn ai_chooses_immediate_win() { " .trim_matches('\n'); let (next_board, val, visited) = choose_move( - Player::Min, + Player(0), &Board::parse(min_will_win).unwrap(), 5, i32::MIN + 1, i32::MAX, ); - let value = Player::Min.sign() * val; + let value = Player(0).direction() * val; assert_eq!(next_board, Some(Board::parse(min_wins).unwrap())); assert_eq!(value, -1000000); assert!(visited > 0);