terran pivot

This commit is contained in:
2026-01-23 15:20:18 -07:00
parent 321e3ebb76
commit 0dfdb6a6df
5 changed files with 294 additions and 76 deletions

View File

@@ -24,23 +24,20 @@ impl BuildStage {
pub fn get_build_stages() -> Vec<BuildStage> {
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),
]
}

View File

@@ -43,5 +43,6 @@ pub struct BuildHistoryEntry {
pub unit_type: Option<UnitType>,
pub upgrade_type: Option<UpgradeType>,
pub assigned_unit_id: Option<usize>,
pub tile_position: Option<rsbwapi::TilePosition>,
// pub status: BuildStatus,
}

View File

@@ -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<Self::Item> {
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<TilePosition> {
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
}

View File

@@ -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<UnitType> {
fn check_need_more_supply(_game: &Game, player: &Player, _state: &GameState) -> Option<UnitType> {
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<rsbwapi::TilePosition>)> {
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);
}
}
}
}
}

View File

@@ -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