From 0dfdb6a6dfc93e5033cdd7f37abe003f84237d7f Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Fri, 23 Jan 2026 15:20:18 -0700 Subject: [PATCH] terran pivot --- protossbot/src/state/build_stages.rs | 31 ++-- protossbot/src/state/game_state.rs | 1 + protossbot/src/utils/build_location_utils.rs | 154 +++++++++++++--- protossbot/src/utils/build_manager.rs | 180 ++++++++++++++++--- scripts/bwapi-preferences.yml | 4 +- 5 files changed, 294 insertions(+), 76 deletions(-) diff --git a/protossbot/src/state/build_stages.rs b/protossbot/src/state/build_stages.rs index 9774b03..e826e49 100644 --- a/protossbot/src/state/build_stages.rs +++ b/protossbot/src/state/build_stages.rs @@ -24,23 +24,20 @@ impl BuildStage { pub fn get_build_stages() -> Vec { vec![ BuildStage::new("Start") - .with_unit(UnitType::Protoss_Probe, 10) - .with_unit(UnitType::Protoss_Pylon, 1), - + .with_unit(UnitType::Terran_SCV, 10) + .with_unit(UnitType::Terran_Supply_Depot, 1), BuildStage::new("Basic Production") - .with_unit(UnitType::Protoss_Probe, 12) - .with_unit(UnitType::Protoss_Pylon, 2) - .with_unit(UnitType::Protoss_Gateway, 1) - .with_unit(UnitType::Protoss_Forge, 1), - - - // Stage 2: Defense cannons - BuildStage::new("Defense Cannons") - .with_unit(UnitType::Protoss_Probe, 16) - .with_unit(UnitType::Protoss_Pylon, 3) - .with_unit(UnitType::Protoss_Nexus, 1) - .with_unit(UnitType::Protoss_Gateway, 1) - .with_unit(UnitType::Protoss_Forge, 1) - .with_unit(UnitType::Protoss_Photon_Cannon, 4), + .with_unit(UnitType::Terran_SCV, 12) + .with_unit(UnitType::Terran_Supply_Depot, 2) + .with_unit(UnitType::Terran_Barracks, 1) + .with_unit(UnitType::Terran_Refinery, 1), + // Stage 2: Defense bunker + BuildStage::new("Defense Bunker") + .with_unit(UnitType::Terran_SCV, 16) + .with_unit(UnitType::Terran_Supply_Depot, 3) + .with_unit(UnitType::Terran_Command_Center, 1) + .with_unit(UnitType::Terran_Barracks, 1) + .with_unit(UnitType::Terran_Refinery, 1) + .with_unit(UnitType::Terran_Bunker, 2), ] } diff --git a/protossbot/src/state/game_state.rs b/protossbot/src/state/game_state.rs index d798bc2..03ecf23 100644 --- a/protossbot/src/state/game_state.rs +++ b/protossbot/src/state/game_state.rs @@ -43,5 +43,6 @@ pub struct BuildHistoryEntry { pub unit_type: Option, pub upgrade_type: Option, pub assigned_unit_id: Option, + pub tile_position: Option, // pub status: BuildStatus, } diff --git a/protossbot/src/utils/build_location_utils.rs b/protossbot/src/utils/build_location_utils.rs index bbd4137..bdeb3ec 100644 --- a/protossbot/src/utils/build_location_utils.rs +++ b/protossbot/src/utils/build_location_utils.rs @@ -1,48 +1,146 @@ -use rsbwapi::{Game, TilePosition, Unit, UnitType}; +use rsbwapi::{Game, Player, TilePosition, Unit, UnitType}; + +// Spiral iterator similar to Styx2's approach +struct Spiral { + center: TilePosition, + x: i32, + y: i32, + dx: i32, + dy: i32, + segment_length: i32, + segment_passed: i32, +} + +impl Spiral { + fn new(center: TilePosition) -> Self { + Self { + center, + x: 0, + y: 0, + dx: 0, + dy: -1, + segment_length: 1, + segment_passed: 0, + } + } +} + +impl Iterator for Spiral { + type Item = TilePosition; + + fn next(&mut self) -> Option { + let result = TilePosition { + x: self.center.x + self.x, + y: self.center.y + self.y, + }; + + // Move to next position + self.x += self.dx; + self.y += self.dy; + self.segment_passed += 1; + + if self.segment_passed == self.segment_length { + self.segment_passed = 0; + + // Turn 90 degrees clockwise + let temp = self.dx; + self.dx = -self.dy; + self.dy = temp; + + // Increase segment length every two turns + if self.dy == 0 { + self.segment_length += 1; + } + } + + Some(result) + } +} pub fn find_build_location( game: &Game, + player: &Player, builder: &Unit, building_type: UnitType, max_range: i32, ) -> Option { - let start_tile = builder.get_tile_position(); + // Find the base to start spiral search from + let start_tile = if let Some(nexus) = player + .get_units() + .iter() + .find(|u| u.get_type() == UnitType::Protoss_Nexus) + { + nexus.get_tile_position() + } else { + builder.get_tile_position() + }; + let map_width = game.map_width(); let map_height = game.map_height(); + let max_tiles = (max_range * max_range) as usize; - for distance in 0..max_range { - for dx in -distance..=distance { - for dy in -distance..=distance { - if dx.abs() != distance && dy.abs() != distance { - continue; - } - - let tile = TilePosition { - x: start_tile.x + dx, - y: start_tile.y + dy, - }; - - if tile.x < 0 || tile.y < 0 || tile.x >= map_width || tile.y >= map_height { - continue; - } - - if is_valid_build_location(game, building_type, tile, builder) { - return Some(tile); - } - } - } - } - - None + // Use spiral search like Styx2 + Spiral::new(start_tile) + .take(max_tiles.min(300)) // Limit to 300 tiles like Styx2 + .filter(|&tile| { + // Check bounds + tile.x >= 0 && tile.y >= 0 && tile.x < map_width && tile.y < map_height + }) + .find(|&tile| is_valid_build_location(game, player, building_type, tile, builder)) } fn is_valid_build_location( game: &Game, + player: &Player, building_type: UnitType, position: TilePosition, builder: &Unit, ) -> bool { - game - .can_build_here(builder, position, building_type, false) + // Get building dimensions + let width = building_type.tile_width(); + let height = building_type.tile_height(); + + let map_width = game.map_width(); + let map_height = game.map_height(); + + // Check if building would fit on the map + if position.x + width > map_width || position.y + height > map_height { + return false; + } + + // Use BWAPI's can_build_here like Styx2 does (it handles most validation) + if !game + .can_build_here(Some(builder), position, building_type, false) .unwrap_or(false) + { + return false; + } + + let center = position.to_position() + + rsbwapi::Position { + x: (width * 32) / 2, + y: (height * 32) / 2, + }; + + // Check no resource containers nearby (minerals/geysers) - 128 pixel radius like Styx2 + let has_resources_nearby = game.get_all_units().iter().any(|u| { + (u.get_type().is_mineral_field() || u.get_type().is_refinery()) + && u.get_position().distance(center) < 128.0 + }); + + if has_resources_nearby { + return false; + } + + // Check no resource depots nearby - 128 pixel radius like Styx2 + let has_depot_nearby = player + .get_units() + .iter() + .any(|u| u.get_type().is_resource_depot() && u.get_position().distance(center) < 128.0); + + if has_depot_nearby { + return false; + } + + true } diff --git a/protossbot/src/utils/build_manager.rs b/protossbot/src/utils/build_manager.rs index 7ce50d1..3ace51d 100644 --- a/protossbot/src/utils/build_manager.rs +++ b/protossbot/src/utils/build_manager.rs @@ -69,22 +69,27 @@ fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) { let builder_id = builder.get_id(); - if assign_builder_to_construct(game, &builder, unit_type, state) { - let entry = BuildHistoryEntry { - unit_type: Some(unit_type), - upgrade_type: None, - assigned_unit_id: Some(builder_id), - }; + if let Some((success, tile_pos)) = + assign_builder_to_construct(game, player, &builder, unit_type, state) + { + if success { + let entry = BuildHistoryEntry { + unit_type: Some(unit_type), + upgrade_type: None, + assigned_unit_id: Some(builder_id), + tile_position: tile_pos, + }; - state.unit_build_history.push(entry); + state.unit_build_history.push(entry); - let current_stage = &state.build_stages[state.current_stage_index]; - println!( - "Started building {} with unit {} (Stage: {})", - unit_type.name(), - builder_id, - current_stage.name - ); + let current_stage = &state.build_stages[state.current_stage_index]; + println!( + "Started building {} with unit {} (Stage: {})", + unit_type.name(), + builder_id, + current_stage.name + ); + } } } @@ -174,7 +179,7 @@ fn get_next_thing_to_build(game: &Game, player: &Player, state: &GameState) -> O .max_by_key(|unit_type| unit_type.mineral_price() + unit_type.gas_price()) } -fn check_need_more_supply(game: &Game, player: &Player, state: &GameState) -> Option { +fn check_need_more_supply(_game: &Game, player: &Player, _state: &GameState) -> Option { let supply_used = player.supply_used(); let supply_total = player.supply_total(); @@ -189,13 +194,7 @@ fn check_need_more_supply(game: &Game, player: &Player, state: &GameState) -> Op let pylon_type = UnitType::Protoss_Pylon; if can_afford_unit(player, pylon_type) { - if let Some(builder) = find_builder_for_unit(player, pylon_type, state) { - let build_location = - build_location_utils::find_build_location(game, &builder, pylon_type, 25); - if build_location.is_some() { - return Some(pylon_type); - } - } + return Some(pylon_type); } } @@ -224,14 +223,16 @@ fn find_builder_for_unit( fn assign_builder_to_construct( game: &Game, + player: &Player, builder: &rsbwapi::Unit, unit_type: UnitType, state: &mut GameState, -) -> bool { +) -> Option<(bool, Option)> { let builder_id = builder.get_id(); if unit_type.is_building() { - let build_location = build_location_utils::find_build_location(game, builder, unit_type, 25); + let build_location = + build_location_utils::find_build_location(game, player, builder, unit_type, 42); if let Some(pos) = build_location { println!( @@ -251,11 +252,11 @@ fn assign_builder_to_construct( target_unit: None, }; state.intended_commands.insert(builder_id, intended_cmd); - true + Some((true, Some(pos))) } Err(e) => { println!("Build command FAILED for {}: {:?}", unit_type.name(), e); - false + Some((false, None)) } } } else { @@ -264,7 +265,7 @@ fn assign_builder_to_construct( unit_type.name(), builder.get_id() ); - false + Some((false, None)) } } else { match builder.train(unit_type) { @@ -275,11 +276,11 @@ fn assign_builder_to_construct( target_unit: None, }; state.intended_commands.insert(builder_id, intended_cmd); - true + Some((true, None)) } Err(e) => { println!("Train command FAILED for {}: {:?}", unit_type.name(), e); - false + Some((false, None)) } } } @@ -375,4 +376,125 @@ pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState) y += 10; } } + + // Draw boxes for pending building assignments + let has_pending = state.unit_build_history.iter().any(|entry| { + if let Some(unit_id) = entry.assigned_unit_id { + state.intended_commands.contains_key(&unit_id) + } else { + false + } + }); + + for entry in &state.unit_build_history { + if let (Some(unit_type), Some(tile_pos), Some(unit_id)) = + (entry.unit_type, entry.tile_position, entry.assigned_unit_id) + { + // Check if the assignment is still active (unit hasn't started building yet) + if state.intended_commands.contains_key(&unit_id) { + // Convert tile position to pixel position + let pixel_x = tile_pos.x * 32; + let pixel_y = tile_pos.y * 32; + + // Get building dimensions in pixels + let width = unit_type.tile_width() * 32; + let height = unit_type.tile_height() * 32; + + // Draw the building outline box + use rsbwapi::{Color, Position}; + let top_left = Position { + x: pixel_x, + y: pixel_y, + }; + let top_right = Position { + x: pixel_x + width, + y: pixel_y, + }; + let bottom_left = Position { + x: pixel_x, + y: pixel_y + height, + }; + let bottom_right = Position { + x: pixel_x + width, + y: pixel_y + height, + }; + + // Draw yellow box for pending buildings + let color = Color::Yellow; + game.draw_line_map(top_left, top_right, color); + game.draw_line_map(top_right, bottom_right, color); + game.draw_line_map(bottom_right, bottom_left, color); + game.draw_line_map(bottom_left, top_left, color); + + // Draw building name at the center + let center = Position { + x: pixel_x + width / 2, + y: pixel_y + height / 2, + }; + game.draw_text_map(center, &format!("Pending: {}", unit_type.name())); + } + } + } + + // Draw buildable locations around nexus if there are pending assignments + if has_pending { + use rsbwapi::{Color, Position, TilePosition}; + + // Find the nexus + if let Some(nexus) = player + .get_units() + .iter() + .find(|u| u.get_type() == UnitType::Protoss_Nexus) + { + let nexus_tile = nexus.get_tile_position(); + let radius = 30; + + // Iterate through tiles in radius around nexus + for dx in -radius..=radius { + for dy in -radius..=radius { + let tile_x = nexus_tile.x + dx; + let tile_y = nexus_tile.y + dy; + + // Check bounds + if tile_x < 0 || tile_y < 0 { + continue; + } + + let tile_pos = TilePosition { + x: tile_x, + y: tile_y, + }; + + // Check if within circular radius + let dist_sq = (dx * dx + dy * dy) as f32; + if dist_sq > (radius * radius) as f32 { + continue; + } + + // Check buildability + let is_buildable = game.is_buildable(tile_pos); + if !is_buildable { + continue; + } + + // Check if powered (check if a 1x1 tile has power) + let has_power = game.has_power(tile_pos, (1, 1)); + + // Draw small box at tile position + let pixel_x = tile_x * 32; + let pixel_y = tile_y * 32; + let center = Position { + x: pixel_x + 16, + y: pixel_y + 16, + }; + + // Green for buildable+powered, Blue for just buildable + let color = if has_power { Color::Green } else { Color::Blue }; + + // Draw small filled circle/box to indicate buildable location + game.draw_circle_map(center, 3, color, true); + } + } + } + } } diff --git a/scripts/bwapi-preferences.yml b/scripts/bwapi-preferences.yml index e4d3a40..938717d 100644 --- a/scripts/bwapi-preferences.yml +++ b/scripts/bwapi-preferences.yml @@ -9,8 +9,8 @@ map: maps/BroodWar/(4)CircuitBreaker.scx # map: maps/(2)Boxer.scm # player_race: Zerg -# player_race: Terran -player_race: Protoss +player_race: Terran +# player_race: Protoss # player_race: Random # enemy_count: 1