Compare commits

5 Commits
main ... terran

Author SHA1 Message Date
810fc4a3d4 building but still bugs 2026-01-23 17:03:01 -07:00
9956c930cd removing intended_commands 2026-01-23 16:40:07 -07:00
dd24f686b3 persisting changes 2026-01-23 16:17:30 -07:00
997cbdb48e better management 2026-01-23 15:31:38 -07:00
0dfdb6a6df terran pivot 2026-01-23 15:20:18 -07:00
8 changed files with 237 additions and 298 deletions

View File

@@ -70,16 +70,16 @@ impl AiModule for ProtosBot {
} }
println!("unit created: {:?}", unit.get_type()); println!("unit created: {:?}", unit.get_type());
// Check if the created unit is a building // If the created unit is a building, handle building creation; otherwise handle unit creation (e.g., trained units).
if !unit.get_type().is_building() {
return;
}
let Ok(mut locked_state) = self.game_state.lock() else { let Ok(mut locked_state) = self.game_state.lock() else {
return; return;
}; };
build_manager::on_building_create(&unit, &mut locked_state); if unit.get_type().is_building() {
build_manager::on_building_create(&unit, &mut locked_state);
} else {
build_manager::on_unit_create(&unit, &mut locked_state);
}
} }
fn on_unit_morph(&mut self, _game: &Game, _unit: Unit) {} fn on_unit_morph(&mut self, _game: &Game, _unit: Unit) {}

View File

@@ -24,23 +24,26 @@ impl BuildStage {
pub fn get_build_stages() -> Vec<BuildStage> { pub fn get_build_stages() -> Vec<BuildStage> {
vec![ vec![
BuildStage::new("Start") BuildStage::new("Start")
.with_unit(UnitType::Protoss_Probe, 10) .with_unit(UnitType::Terran_SCV, 10)
.with_unit(UnitType::Protoss_Pylon, 1), .with_unit(UnitType::Terran_Supply_Depot, 1),
BuildStage::new("Basic Production") BuildStage::new("Basic Production")
.with_unit(UnitType::Protoss_Probe, 12) .with_unit(UnitType::Terran_SCV, 12)
.with_unit(UnitType::Protoss_Pylon, 2) .with_unit(UnitType::Terran_Supply_Depot, 2)
.with_unit(UnitType::Protoss_Gateway, 1) .with_unit(UnitType::Terran_Barracks, 1)
.with_unit(UnitType::Protoss_Forge, 1), .with_unit(UnitType::Terran_Refinery, 1),
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),
BuildStage::new("Mid Game")
// Stage 2: Defense cannons .with_unit(UnitType::Terran_SCV, 20)
BuildStage::new("Defense Cannons") .with_unit(UnitType::Terran_Supply_Depot, 4)
.with_unit(UnitType::Protoss_Probe, 16) .with_unit(UnitType::Terran_Command_Center, 2)
.with_unit(UnitType::Protoss_Pylon, 3) .with_unit(UnitType::Terran_Barracks, 2)
.with_unit(UnitType::Protoss_Nexus, 1) .with_unit(UnitType::Terran_Refinery, 2)
.with_unit(UnitType::Protoss_Gateway, 1) .with_unit(UnitType::Terran_Missile_Turret, 2),
.with_unit(UnitType::Protoss_Forge, 1)
.with_unit(UnitType::Protoss_Photon_Cannon, 4),
] ]
} }

View File

@@ -1,10 +1,9 @@
use rsbwapi::{Order, Position, Unit, UnitType, UpgradeType}; use rsbwapi::{UnitType, UpgradeType};
use std::collections::HashMap; use std::collections::HashMap;
use crate::state::build_stages::BuildStage; use crate::state::build_stages::BuildStage;
pub struct GameState { pub struct GameState {
pub intended_commands: HashMap<usize, IntendedCommand>,
pub unit_build_history: Vec<BuildHistoryEntry>, pub unit_build_history: Vec<BuildHistoryEntry>,
pub build_stages: Vec<BuildStage>, pub build_stages: Vec<BuildStage>,
pub current_stage_index: usize, pub current_stage_index: usize,
@@ -15,7 +14,6 @@ pub struct GameState {
impl Default for GameState { impl Default for GameState {
fn default() -> Self { fn default() -> Self {
Self { Self {
intended_commands: HashMap::new(),
unit_build_history: Vec::new(), unit_build_history: Vec::new(),
build_stages: crate::state::build_stages::get_build_stages(), build_stages: crate::state::build_stages::get_build_stages(),
current_stage_index: 0, current_stage_index: 0,
@@ -25,13 +23,6 @@ impl Default for GameState {
} }
} }
#[derive(Clone, Debug)]
pub struct IntendedCommand {
pub order: Order,
pub target_position: Option<Position>,
pub target_unit: Option<Unit>,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum BuildStatus { pub enum BuildStatus {
Assigned, Assigned,
@@ -43,5 +34,6 @@ pub struct BuildHistoryEntry {
pub unit_type: Option<UnitType>, pub unit_type: Option<UnitType>,
pub upgrade_type: Option<UpgradeType>, pub upgrade_type: Option<UpgradeType>,
pub assigned_unit_id: Option<usize>, pub assigned_unit_id: Option<usize>,
pub tile_position: Option<rsbwapi::TilePosition>,
// pub status: BuildStatus, // pub status: BuildStatus,
} }

View File

@@ -1,48 +1,46 @@
use rsbwapi::{Game, TilePosition, Unit, UnitType}; use rsbwapi::{Game, Player, TilePosition, Unit, UnitType};
pub fn find_build_location( pub fn find_build_location(
game: &Game, game: &Game,
_player: &Player,
builder: &Unit, builder: &Unit,
building_type: UnitType, building_type: UnitType,
max_range: i32, max_range: i32,
) -> Option<TilePosition> { ) -> Option<TilePosition> {
let start_tile = builder.get_tile_position(); if building_type.is_refinery() {
let map_width = game.map_width(); for geyser in game.get_geysers() {
let map_height = game.map_height(); let tp = geyser.get_tile_position();
if let Ok(true) = game.can_build_here(Some(builder), tp, building_type, false) {
return Some(tp);
}
}
return None;
}
for distance in 0..max_range { let start = builder.get_tile_position();
for dx in -distance..=distance { for radius in 0..=max_range {
for dy in -distance..=distance { for dx in -radius..=radius {
if dx.abs() != distance && dy.abs() != distance { let dy = radius - dx.abs();
continue; for &dy_sign in &[-1, 1] {
} let cand = TilePosition {
x: start.x + dx,
let tile = TilePosition { y: start.y + dy * dy_sign,
x: start_tile.x + dx,
y: start_tile.y + dy,
}; };
if let Ok(true) = game.can_build_here(Some(builder), cand, building_type, false) {
if tile.x < 0 || tile.y < 0 || tile.x >= map_width || tile.y >= map_height { return Some(cand);
}
if dy == 0 {
continue; continue;
} }
let cand2 = TilePosition {
if is_valid_build_location(game, building_type, tile, builder) { x: start.x + dx,
return Some(tile); y: start.y - dy * dy_sign,
};
if let Ok(true) = game.can_build_here(Some(builder), cand2, building_type, false) {
return Some(cand2);
} }
} }
} }
} }
None None
} }
fn is_valid_build_location(
game: &Game,
building_type: UnitType,
position: TilePosition,
builder: &Unit,
) -> bool {
game
.can_build_here(builder, position, building_type, false)
.unwrap_or(false)
}

View File

@@ -1,108 +1,114 @@
use rsbwapi::{Game, Order, Player, Unit, UnitType}; use rsbwapi::{Game, Player, Unit, UnitType};
use crate::{ use crate::{
state::game_state::{BuildHistoryEntry, GameState, IntendedCommand}, state::game_state::{BuildHistoryEntry, GameState},
utils::build_location_utils, utils::build_location_utils,
}; };
pub fn on_frame(game: &Game, player: &Player, state: &mut GameState) { pub fn on_frame(game: &Game, player: &Player, state: &mut GameState) {
cleanup_stale_commands(player, state); // No intended command tracking; directly manage builds and stages.
check_and_advance_stage(player, state); check_and_advance_stage(player, state);
state.stage_item_status = get_status_for_stage_items(game, player, state); state.stage_item_status = get_status_for_stage_items(game, player, state);
try_start_next_build(game, player, state); try_start_next_build(game, player, state);
} }
pub fn on_building_create(unit: &Unit, state: &mut GameState) { pub fn on_building_create(unit: &Unit, state: &mut GameState) {
if let Some(entry) = state // When a building is created, remove the corresponding build history entry so the next
.unit_build_history // build can be started without waiting for the current one to finish.
.iter() if let Some(pos) = state.unit_build_history.iter().position(|entry| {
.rev() entry
.find(|e| e.unit_type == Some(unit.get_type())) .unit_type
{ .map(|ut| ut == unit.get_type())
if let Some(probe_id) = entry.assigned_unit_id { .unwrap_or(false)
// Remove the probe's intended command (PlaceBuilding order) }) {
state.intended_commands.remove(&probe_id); state.unit_build_history.remove(pos);
println!(
"Building {} started. Removed assignment for probe {}",
unit.get_type().name(),
probe_id
);
}
} }
} }
fn cleanup_stale_commands(player: &Player, state: &mut GameState) { /// Called when a nonbuilding unit (e.g., a trained unit) is created.
let unit_ids: Vec<usize> = player.get_units().iter().map(|u| u.get_id()).collect(); /// This clears any pending assignment for the building that trained it.
pub fn on_unit_create(_unit: &Unit, _state: &mut GameState) {
state.intended_commands.retain(|unit_id, cmd| { // No intended command tracking needed for unit creation.
// Remove if unit no longer exists
if !unit_ids.contains(unit_id) {
return false;
}
// Find the unit
if let Some(unit) = player.get_units().iter().find(|u| u.get_id() == *unit_id) {
// For PlaceBuilding orders, check if unit is actually constructing or idle
if cmd.order == Order::PlaceBuilding {
// Keep command only if unit is moving to build location or constructing
return unit.is_constructing() || unit.get_order() == Order::PlaceBuilding;
}
// For Train orders, check if the building is training
if cmd.order == Order::Train {
return unit.is_training();
}
}
false
});
} }
fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) { fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) {
if !should_start_next_build(game, player, state) {
return;
}
let Some(unit_type) = get_next_thing_to_build(game, player, state) else { let Some(unit_type) = get_next_thing_to_build(game, player, state) else {
return; return;
}; };
let Some(builder) = find_builder_for_unit(player, unit_type, state) else { let Some(builder) = find_builder_for_unit(player, unit_type, state) else {
return; return;
}; };
let builder_id = builder.get_id(); let builder_id = builder.get_id();
if let Some((success, tile_pos)) =
if assign_builder_to_construct(game, &builder, unit_type, state) { assign_builder_to_construct(game, player, &builder, unit_type, state)
let entry = BuildHistoryEntry { {
unit_type: Some(unit_type), if success {
upgrade_type: None, let entry = BuildHistoryEntry {
assigned_unit_id: Some(builder_id), unit_type: Some(unit_type),
}; upgrade_type: None,
assigned_unit_id: Some(builder_id),
state.unit_build_history.push(entry); tile_position: tile_pos,
};
let current_stage = &state.build_stages[state.current_stage_index]; state.unit_build_history.push(entry);
println!( }
"Started building {} with unit {} (Stage: {})",
unit_type.name(),
builder_id,
current_stage.name
);
} }
} }
fn should_start_next_build(_game: &Game, player: &Player, state: &mut GameState) -> bool {
// Do not start a new build if there is a pending build (assigned but not yet constructing/training).
if pending_build_exists(state, player) {
return false;
}
// Ensure there is a builder available for the next thing to build.
if let Some(unit_type) = get_next_thing_to_build(_game, player, state) {
find_builder_for_unit(player, unit_type, state).is_some()
} else {
false
}
}
fn has_ongoing_constructions(state: &GameState, player: &Player) -> bool {
// Consider a construction ongoing if there is a build history entry with an assigned unit that
// is currently constructing or training. This covers the period after a build command is issued
// but before the unit starts the actual constructing state, preventing multiple workers from
// being assigned to the same build command.
state.unit_build_history.iter().any(|entry| {
if let Some(unit_id) = entry.assigned_unit_id {
if let Some(unit) = player.get_units().iter().find(|u| u.get_id() == unit_id) {
return unit.is_constructing() || unit.is_training();
}
}
false
})
}
// Returns true if there is a build history entry with an assigned builder that has not yet started constructing or training.
fn pending_build_exists(state: &GameState, player: &Player) -> bool {
state.unit_build_history.iter().any(|entry| {
if let Some(unit_id) = entry.assigned_unit_id {
if let Some(unit) = player.get_units().iter().find(|u| u.get_id() == unit_id) {
return !(unit.is_constructing() || unit.is_training());
}
}
false
})
}
fn get_status_for_stage_items( fn get_status_for_stage_items(
_game: &Game, _game: &Game,
player: &Player, player: &Player,
state: &GameState, state: &GameState,
) -> std::collections::HashMap<String, String> { ) -> std::collections::HashMap<String, String> {
let mut status_map = std::collections::HashMap::new(); let mut status_map = std::collections::HashMap::new();
let Some(current_stage) = state.build_stages.get(state.current_stage_index) else { let Some(current_stage) = state.build_stages.get(state.current_stage_index) else {
return status_map; return status_map;
}; };
for (unit_type, &desired_count) in &current_stage.desired_counts { for (unit_type, &desired_count) in &current_stage.desired_counts {
let unit_name = unit_type.name().to_string(); let unit_name = unit_type.name().to_string();
let current_count = count_units_of_type(player, state, *unit_type); let current_count = count_units_of_type(player, state, *unit_type);
if current_count >= desired_count { if current_count >= desired_count {
status_map.insert( status_map.insert(
unit_name, unit_name,
@@ -110,7 +116,6 @@ fn get_status_for_stage_items(
); );
continue; continue;
} }
if !can_afford_unit(player, *unit_type) { if !can_afford_unit(player, *unit_type) {
let minerals_short = unit_type.mineral_price() - player.minerals(); let minerals_short = unit_type.mineral_price() - player.minerals();
let gas_short = unit_type.gas_price() - player.gas(); let gas_short = unit_type.gas_price() - player.gas();
@@ -126,7 +131,6 @@ fn get_status_for_stage_items(
); );
continue; continue;
} }
if unit_type.is_building() { if unit_type.is_building() {
if find_builder_for_unit(player, *unit_type, state).is_none() { if find_builder_for_unit(player, *unit_type, state).is_none() {
status_map.insert( status_map.insert(
@@ -136,79 +140,59 @@ fn get_status_for_stage_items(
continue; continue;
} }
} }
status_map.insert( status_map.insert(
unit_name, unit_name,
format!("Ready to build ({}/{})", current_count, desired_count), format!("Ready to build ({}/{})", current_count, desired_count),
); );
} }
status_map status_map
} }
fn get_next_thing_to_build(game: &Game, player: &Player, state: &GameState) -> Option<UnitType> { fn get_next_thing_to_build(game: &Game, player: &Player, state: &GameState) -> Option<UnitType> {
let current_stage = state.build_stages.get(state.current_stage_index)?; let current_stage = state.build_stages.get(state.current_stage_index)?;
if let Some(pylon) = check_need_more_supply(game, player, state) { if let Some(pylon) = check_need_more_supply(game, player, state) {
return Some(pylon); return Some(pylon);
} }
let status_map = get_status_for_stage_items(game, player, state); let status_map = get_status_for_stage_items(game, player, state);
let mut candidates = Vec::new(); let mut candidates = Vec::new();
for (unit_type, &desired_count) in &current_stage.desired_counts { for (unit_type, &desired_count) in &current_stage.desired_counts {
let current_count = count_units_of_type(player, state, *unit_type); let current_count = count_units_of_type(player, state, *unit_type);
if current_count >= desired_count { if current_count >= desired_count {
continue; continue;
} }
let status = status_map.get(&unit_type.name().to_string()); let status = status_map.get(&unit_type.name().to_string());
if status.is_some() && status.unwrap().starts_with("Ready to build") { if status.is_some() && status.unwrap().starts_with("Ready to build") {
candidates.push(*unit_type); candidates.push(*unit_type);
} }
} }
candidates candidates
.into_iter() .into_iter()
.max_by_key(|unit_type| unit_type.mineral_price() + unit_type.gas_price()) .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_used = player.supply_used();
let supply_total = player.supply_total(); let supply_total = player.supply_total();
if supply_total == 0 { if supply_total == 0 {
return None; return None;
} }
let supply_remaining = supply_total - supply_used; let supply_remaining = supply_total - supply_used;
let threshold = ((supply_total as f32) * 0.15).ceil() as i32; let threshold = ((supply_total as f32) * 0.15).ceil() as i32;
if supply_remaining <= threshold && supply_total < 400 { if supply_remaining <= threshold && supply_total < 400 {
let pylon_type = UnitType::Protoss_Pylon; let pylon_type = UnitType::Protoss_Pylon;
if can_afford_unit(player, pylon_type) { if can_afford_unit(player, pylon_type) {
if let Some(builder) = find_builder_for_unit(player, pylon_type, state) { return Some(pylon_type);
let build_location =
build_location_utils::find_build_location(game, &builder, pylon_type, 25);
if build_location.is_some() {
return Some(pylon_type);
}
}
} }
} }
None None
} }
fn find_builder_for_unit( fn find_builder_for_unit(
player: &Player, player: &Player,
unit_type: UnitType, unit_type: UnitType,
state: &GameState, _state: &GameState,
) -> Option<rsbwapi::Unit> { ) -> Option<rsbwapi::Unit> {
let builder_type = unit_type.what_builds().0; let builder_type = unit_type.what_builds().0;
player player
.get_units() .get_units()
.iter() .iter()
@@ -217,45 +201,37 @@ fn find_builder_for_unit(
&& !u.is_constructing() && !u.is_constructing()
&& !u.is_training() && !u.is_training()
&& (u.is_idle() || u.is_gathering_minerals() || u.is_gathering_gas()) && (u.is_idle() || u.is_gathering_minerals() || u.is_gathering_gas())
&& !state.intended_commands.contains_key(&u.get_id())
}) })
.cloned() .cloned()
} }
fn assign_builder_to_construct( fn assign_builder_to_construct(
game: &Game, game: &Game,
player: &Player,
builder: &rsbwapi::Unit, builder: &rsbwapi::Unit,
unit_type: UnitType, unit_type: UnitType,
state: &mut GameState, state: &mut GameState,
) -> bool { ) -> Option<(bool, Option<rsbwapi::TilePosition>)> {
let builder_id = builder.get_id(); let builder_id = builder.get_id();
if unit_type.is_building() { 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 { if let Some(pos) = build_location {
println!( println!(
"Attempting to build {} at {:?} with worker {} (currently at {:?})", "Attempting to build {} at {:?} with worker {}",
unit_type.name(), unit_type.name(),
pos, pos,
builder_id, builder_id,
builder.get_position()
); );
match builder.build(unit_type, pos) { match builder.build(unit_type, pos) {
Ok(_) => { Ok(_) => {
println!("Build command succeeded for {}", unit_type.name()); println!("Build command succeeded for {}", unit_type.name());
let intended_cmd = IntendedCommand { // No intended command for building actions; the builder (worker) will be tracked via ongoing constructions.
order: Order::PlaceBuilding, Some((true, Some(pos)))
target_position: Some(pos.to_position()),
target_unit: None,
};
state.intended_commands.insert(builder_id, intended_cmd);
true
} }
Err(e) => { Err(e) => {
println!("Build command FAILED for {}: {:?}", unit_type.name(), e); println!("Build command FAILED for {}: {:?}", unit_type.name(), e);
false Some((false, None))
} }
} }
} else { } else {
@@ -264,41 +240,33 @@ fn assign_builder_to_construct(
unit_type.name(), unit_type.name(),
builder.get_id() builder.get_id()
); );
false Some((false, None))
} }
} else { } else {
match builder.train(unit_type) { match builder.train(unit_type) {
Ok(_) => { Ok(_) => {
let intended_cmd = IntendedCommand { // No intended command for training; the building will be tracked via ongoing constructions.
order: Order::Train, Some((true, None))
target_position: None,
target_unit: None,
};
state.intended_commands.insert(builder_id, intended_cmd);
true
} }
Err(e) => { Err(e) => {
println!("Train command FAILED for {}: {:?}", unit_type.name(), e); println!("Train command FAILED for {}: {:?}", unit_type.name(), e);
false Some((false, None))
} }
} }
} }
} }
fn count_units_of_type(player: &Player, _state: &GameState, unit_type: UnitType) -> i32 { fn count_units_of_type(player: &Player, _state: &GameState, unit_type: UnitType) -> i32 {
let existing = player player
.get_units() .get_units()
.iter() .iter()
.filter(|u| u.get_type() == unit_type) .filter(|u| u.get_type() == unit_type)
.count() as i32; .count() as i32
existing
} }
fn can_afford_unit(player: &Player, unit_type: UnitType) -> bool { fn can_afford_unit(player: &Player, unit_type: UnitType) -> bool {
let minerals = player.minerals(); let minerals = player.minerals();
let gas = player.gas(); let gas = player.gas();
minerals >= unit_type.mineral_price() && gas >= unit_type.gas_price() minerals >= unit_type.mineral_price() && gas >= unit_type.gas_price()
} }
@@ -306,7 +274,6 @@ fn check_and_advance_stage(player: &Player, state: &mut GameState) {
let Some(current_stage) = state.build_stages.get(state.current_stage_index) else { let Some(current_stage) = state.build_stages.get(state.current_stage_index) else {
return; return;
}; };
let stage_complete = current_stage let stage_complete = current_stage
.desired_counts .desired_counts
.iter() .iter()
@@ -314,10 +281,8 @@ fn check_and_advance_stage(player: &Player, state: &mut GameState) {
let current_count = count_units_of_type(player, state, *unit_type); let current_count = count_units_of_type(player, state, *unit_type);
current_count >= desired_count current_count >= desired_count
}); });
if stage_complete { if stage_complete {
let next_stage_index = state.current_stage_index + 1; let next_stage_index = state.current_stage_index + 1;
if next_stage_index < state.build_stages.len() { if next_stage_index < state.build_stages.len() {
println!( println!(
"Stage '{}' complete! Advancing to stage {}", "Stage '{}' complete! Advancing to stage {}",
@@ -331,7 +296,6 @@ fn check_and_advance_stage(player: &Player, state: &mut GameState) {
pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState) { pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState) {
let mut y = 10; let mut y = 10;
let x = 3; let x = 3;
if let Some(current_stage) = state.build_stages.get(state.current_stage_index) { if let Some(current_stage) = state.build_stages.get(state.current_stage_index) {
let next_build = get_next_thing_to_build(game, player, state); let next_build = get_next_thing_to_build(game, player, state);
let next_build_str = if let Some(unit_type) = next_build { let next_build_str = if let Some(unit_type) = next_build {
@@ -348,7 +312,6 @@ pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState)
}; };
game.draw_text_screen((x, y), &next_build_str); game.draw_text_screen((x, y), &next_build_str);
y += 10; y += 10;
if let Some(last_entry) = state.unit_build_history.last() { if let Some(last_entry) = state.unit_build_history.last() {
let unit_name = if let Some(unit_type) = last_entry.unit_type { let unit_name = if let Some(unit_type) = last_entry.unit_type {
unit_type.name() unit_type.name()
@@ -362,10 +325,8 @@ pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState)
game.draw_text_screen((x, y), "Last Built: None"); game.draw_text_screen((x, y), "Last Built: None");
} }
y += 10; y += 10;
game.draw_text_screen((x, y), "Stage Progress:"); game.draw_text_screen((x, y), "Stage Progress:");
y += 10; y += 10;
for (unit_type, &desired_count) in &current_stage.desired_counts { for (unit_type, &desired_count) in &current_stage.desired_counts {
let current_count = count_units_of_type(player, state, *unit_type); let current_count = count_units_of_type(player, state, *unit_type);
game.draw_text_screen( game.draw_text_screen(

View File

@@ -1,17 +1,28 @@
use rsbwapi::{Game, Order, Player, Unit}; use rsbwapi::{Game, Player, Unit};
use crate::state::game_state::{GameState, IntendedCommand}; use crate::state::game_state::GameState;
pub fn assign_idle_workers_to_minerals(game: &Game, player: &Player, state: &mut GameState) { pub fn assign_idle_workers_to_minerals(game: &Game, player: &Player, state: &mut GameState) {
let all_units = player.get_units(); let all_units = player.get_units();
let workers: Vec<Unit> = all_units let workers: Vec<Unit> = all_units
.iter() .iter()
.filter(|u| u.get_type().is_worker() && u.is_completed()) .filter(|u| {
// Worker must be a completed worker unit
u.get_type().is_worker() && u.is_completed()
})
.cloned() .cloned()
.collect(); .collect();
// Assign idle workers to mining // Assign idle workers that are not already assigned in the build history
for worker in workers { for worker in workers {
// Skip if this worker is already recorded as assigned in any ongoing build history entry
let already_assigned = state.unit_build_history.iter().any(|entry| {
entry.assigned_unit_id == Some(worker.get_id())
&& entry.unit_type.map(|ut| ut.is_building()).unwrap_or(false)
});
if already_assigned {
continue;
}
assign_worker_to_mineral(game, &worker, state); assign_worker_to_mineral(game, &worker, state);
} }
} }
@@ -19,76 +30,42 @@ pub fn assign_idle_workers_to_minerals(game: &Game, player: &Player, state: &mut
fn assign_worker_to_mineral(game: &Game, worker: &Unit, state: &mut GameState) { fn assign_worker_to_mineral(game: &Game, worker: &Unit, state: &mut GameState) {
let worker_id = worker.get_id(); let worker_id = worker.get_id();
if let Some(cmd) = state.intended_commands.get(&worker_id) { // Skip if the worker is currently assigned to an inprogress building construction.
if cmd.order != Order::MiningMinerals { let has_in_progress_build = state.unit_build_history.iter().any(|entry| {
return; entry.assigned_unit_id == Some(worker_id)
} && entry.unit_type.map(|ut| ut.is_building()).unwrap_or(false)
} });
if has_in_progress_build {
if !worker.is_idle() {
return; return;
} }
if worker.is_gathering_minerals() || worker.is_gathering_gas() { // Worker must be idle and not already gathering resources.
if !worker.is_idle() || worker.is_gathering_minerals() || worker.is_gathering_gas() {
return; return;
} }
// Find a mineral patch to assign.
let Some(mineral) = find_available_mineral(game, worker, state) else { let Some(mineral) = find_available_mineral(game, worker, state) else {
return; return;
}; };
let intended_cmd = IntendedCommand { // Assign worker to gather minerals (ignore intended command tracking).
order: Order::MiningMinerals,
target_position: None,
target_unit: Some(mineral.clone()),
};
state.intended_commands.insert(worker_id, intended_cmd);
println!(
"Worker {} current order: {:?}, assigning to mine from mineral at {:?}",
worker_id,
worker.get_order(),
mineral.get_position()
);
if worker.gather(&mineral).is_ok() { if worker.gather(&mineral).is_ok() {
println!( println!("Assigned worker {} to mine from mineral", worker_id,);
"Assigned worker {} to mine from mineral at {:?}",
worker_id,
mineral.get_position()
);
} }
} }
fn find_available_mineral(game: &Game, worker: &Unit, state: &GameState) -> Option<Unit> { fn find_available_mineral(game: &Game, worker: &Unit, _state: &GameState) -> Option<Unit> {
let worker_pos = worker.get_position(); let worker_pos = worker.get_position();
let minerals = game.get_static_minerals(); let minerals = game.get_static_minerals();
let mut mineral_list: Vec<Unit> = minerals.iter().filter(|m| m.exists()).cloned().collect(); let mut mineral_list: Vec<Unit> = minerals.iter().filter(|m| m.exists()).cloned().collect();
// Sort minerals by distance to the worker.
mineral_list.sort_by_key(|m| { mineral_list.sort_by_key(|m| {
let pos = m.get_position(); let pos = m.get_position();
((pos.x - worker_pos.x).pow(2) + (pos.y - worker_pos.y).pow(2)) as i32 ((pos.x - worker_pos.x).pow(2) + (pos.y - worker_pos.y).pow(2)) as i32
}); });
for mineral in mineral_list.iter() { // Return the closest mineral, ignoring any intended command tracking.
let mineral_id: usize = mineral.get_id();
let worker_count = state
.intended_commands
.values()
.filter(|cmd| {
if let Some(target) = &cmd.target_unit {
target.get_id() == mineral_id
} else {
false
}
})
.count();
if worker_count < 2 {
return Some(mineral.clone());
}
}
mineral_list.first().cloned() mineral_list.first().cloned()
} }

View File

@@ -31,13 +31,6 @@
width: 100%; width: 100%;
} }
h1 {
color: #d4af37;
margin-bottom: 30px;
font-size: 28px;
text-align: center;
}
.control-group { .control-group {
margin-bottom: 30px; margin-bottom: 30px;
} }
@@ -60,13 +53,17 @@
font-weight: bold; font-weight: bold;
color: #d4af37; color: #d4af37;
display: block; display: block;
transition: color 0.3s;
}
.speed-fast {
color: #4ade80; /* green */
}
.speed-slow {
color: #f87171; /* red */
} }
.speed-label { .speed-label {
color: #999; display: none; /* hide label for compact view */
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
} }
.preset-buttons { .preset-buttons {
@@ -87,6 +84,10 @@
background: #2d2d2d; background: #2d2d2d;
color: #d4af37; color: #d4af37;
} }
button.active {
background: #d4af37;
color: #1e1e1e;
}
button small { button small {
display: block; display: block;
@@ -201,27 +202,28 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>⚡ Game Speed Control</h1>
<div class="speed-display">
<span class="speed-value" id="currentSpeed">42</span>
<span class="speed-label">Current Speed</span>
</div>
<div class="control-group"> <div class="control-group">
<label>Select Speed</label>
<div class="preset-buttons"> <div class="preset-buttons">
<button onclick="setSpeed(42)">42<br /><small>Slowest</small></button> <button data-speed="42" onclick="setSpeed(42)">
<button onclick="setSpeed(1)">1<br /><small>Fast</small></button> 42<br /><small>Slowest</small>
<button onclick="setSpeed(0)">0<br /><small>Fastest</small></button> </button>
<button onclick="setSpeed(-1)">-1<br /><small>Max</small></button> <button data-speed="1" onclick="setSpeed(1)">
1<br /><small>Fast</small>
</button>
<button data-speed="0" onclick="setSpeed(0)">
0<br /><small>Fastest</small>
</button>
<button data-speed="-1" onclick="setSpeed(-1)">
-1<br /><small>Max</small>
</button>
</div> </div>
</div> </div>
<div id="status" class="status"></div> <div id="status" class="status"></div>
<div class="build-status-section"> <div class="build-status-section">
<h2>📋 Build Status</h2>
<div class="stage-info"> <div class="stage-info">
<div class="stage-name" id="stageName">Loading...</div> <div class="stage-name" id="stageName">Loading...</div>
<ul class="build-items" id="buildItems"> <ul class="build-items" id="buildItems">
@@ -232,46 +234,52 @@
</div> </div>
<script id="game-speed-script"> <script id="game-speed-script">
const currentSpeedDisplay = document.getElementById("currentSpeed"); const presetButtons = document.querySelectorAll('.preset-buttons button');
const statusDiv = document.getElementById("status"); const statusDiv = document.getElementById('status');
async function fetchCurrentSpeed() { function highlightButton(speed) {
try { presetButtons.forEach(btn => {
const response = await fetch("http://127.0.0.1:3333/api/speed"); if (Number(btn.dataset.speed) === speed) {
if (response.ok) { btn.classList.add('active');
const data = await response.json(); } else {
updateSpeedDisplay(data.speed); btn.classList.remove('active');
} }
} catch (error) {
showStatus("Unable to connect to bot", "error");
}
}
function updateSpeedDisplay(speed) {
currentSpeedDisplay.textContent = speed;
}
async function setSpeed(speed) {
try {
const response = await fetch("http://127.0.0.1:3333/api/speed", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ speed: speed }),
}); });
}
async function fetchCurrentSpeed() {
const response = await fetch('http://127.0.0.1:3333/api/speed');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
updateSpeedDisplay(data.speed); highlightButton(data.speed);
showStatus(`Speed set to ${data.speed}`, "success"); // Clear any previous error message
statusDiv.textContent = '';
statusDiv.className = 'status';
} else { } else {
showStatus("Failed to update speed", "error"); showStatus('Unable to connect to bot', 'error');
} }
} catch (error) {
showStatus("Connection error", "error");
} }
}
async function setSpeed(speed) {
const response = await fetch('http://127.0.0.1:3333/api/speed', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ speed: speed }),
});
if (response.ok) {
const data = await response.json();
highlightButton(data.speed);
// Clear any previous status message on success
statusDiv.textContent = '';
statusDiv.className = 'status';
} else {
showStatus('Failed to update speed', 'error');
}
}
function showStatus(message, type) { function showStatus(message, type) {
statusDiv.textContent = message; statusDiv.textContent = message;

View File

@@ -9,8 +9,8 @@ map: maps/BroodWar/(4)CircuitBreaker.scx
# map: maps/(2)Boxer.scm # map: maps/(2)Boxer.scm
# player_race: Zerg # player_race: Zerg
# player_race: Terran player_race: Terran
player_race: Protoss # player_race: Protoss
# player_race: Random # player_race: Random
# enemy_count: 1 # enemy_count: 1