Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 810fc4a3d4 | |||
| 9956c930cd | |||
| dd24f686b3 | |||
| 997cbdb48e | |||
| 0dfdb6a6df |
@@ -70,16 +70,16 @@ impl AiModule for ProtosBot {
|
||||
}
|
||||
println!("unit created: {:?}", unit.get_type());
|
||||
|
||||
// Check if the created unit is a building
|
||||
if !unit.get_type().is_building() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the created unit is a building, handle building creation; otherwise handle unit creation (e.g., trained units).
|
||||
let Ok(mut locked_state) = self.game_state.lock() else {
|
||||
return;
|
||||
};
|
||||
|
||||
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) {}
|
||||
|
||||
@@ -24,23 +24,26 @@ 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),
|
||||
.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),
|
||||
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),
|
||||
|
||||
|
||||
// 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),
|
||||
BuildStage::new("Mid Game")
|
||||
.with_unit(UnitType::Terran_SCV, 20)
|
||||
.with_unit(UnitType::Terran_Supply_Depot, 4)
|
||||
.with_unit(UnitType::Terran_Command_Center, 2)
|
||||
.with_unit(UnitType::Terran_Barracks, 2)
|
||||
.with_unit(UnitType::Terran_Refinery, 2)
|
||||
.with_unit(UnitType::Terran_Missile_Turret, 2),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use rsbwapi::{Order, Position, Unit, UnitType, UpgradeType};
|
||||
use rsbwapi::{UnitType, UpgradeType};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::state::build_stages::BuildStage;
|
||||
|
||||
pub struct GameState {
|
||||
pub intended_commands: HashMap<usize, IntendedCommand>,
|
||||
pub unit_build_history: Vec<BuildHistoryEntry>,
|
||||
pub build_stages: Vec<BuildStage>,
|
||||
pub current_stage_index: usize,
|
||||
@@ -15,7 +14,6 @@ pub struct GameState {
|
||||
impl Default for GameState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
intended_commands: HashMap::new(),
|
||||
unit_build_history: Vec::new(),
|
||||
build_stages: crate::state::build_stages::get_build_stages(),
|
||||
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)]
|
||||
pub enum BuildStatus {
|
||||
Assigned,
|
||||
@@ -43,5 +34,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,
|
||||
}
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
use rsbwapi::{Game, TilePosition, Unit, UnitType};
|
||||
use rsbwapi::{Game, Player, TilePosition, Unit, UnitType};
|
||||
|
||||
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();
|
||||
let map_width = game.map_width();
|
||||
let map_height = game.map_height();
|
||||
|
||||
for distance in 0..max_range {
|
||||
for dx in -distance..=distance {
|
||||
for dy in -distance..=distance {
|
||||
if dx.abs() != distance && dy.abs() != distance {
|
||||
continue;
|
||||
if building_type.is_refinery() {
|
||||
for geyser in game.get_geysers() {
|
||||
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;
|
||||
}
|
||||
|
||||
let tile = TilePosition {
|
||||
x: start_tile.x + dx,
|
||||
y: start_tile.y + dy,
|
||||
let start = builder.get_tile_position();
|
||||
for radius in 0..=max_range {
|
||||
for dx in -radius..=radius {
|
||||
let dy = radius - dx.abs();
|
||||
for &dy_sign in &[-1, 1] {
|
||||
let cand = TilePosition {
|
||||
x: start.x + dx,
|
||||
y: start.y + dy * dy_sign,
|
||||
};
|
||||
|
||||
if tile.x < 0 || tile.y < 0 || tile.x >= map_width || tile.y >= map_height {
|
||||
if let Ok(true) = game.can_build_here(Some(builder), cand, building_type, false) {
|
||||
return Some(cand);
|
||||
}
|
||||
if dy == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_valid_build_location(game, building_type, tile, builder) {
|
||||
return Some(tile);
|
||||
let cand2 = TilePosition {
|
||||
x: start.x + dx,
|
||||
y: start.y - dy * dy_sign,
|
||||
};
|
||||
if let Ok(true) = game.can_build_here(Some(builder), cand2, building_type, false) {
|
||||
return Some(cand2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,92 +1,101 @@
|
||||
use rsbwapi::{Game, Order, Player, Unit, UnitType};
|
||||
use rsbwapi::{Game, Player, Unit, UnitType};
|
||||
|
||||
use crate::{
|
||||
state::game_state::{BuildHistoryEntry, GameState, IntendedCommand},
|
||||
state::game_state::{BuildHistoryEntry, GameState},
|
||||
utils::build_location_utils,
|
||||
};
|
||||
|
||||
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);
|
||||
state.stage_item_status = get_status_for_stage_items(game, player, state);
|
||||
|
||||
try_start_next_build(game, player, state);
|
||||
}
|
||||
|
||||
pub fn on_building_create(unit: &Unit, state: &mut GameState) {
|
||||
if let Some(entry) = state
|
||||
.unit_build_history
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|e| e.unit_type == Some(unit.get_type()))
|
||||
{
|
||||
if let Some(probe_id) = entry.assigned_unit_id {
|
||||
// Remove the probe's intended command (PlaceBuilding order)
|
||||
state.intended_commands.remove(&probe_id);
|
||||
println!(
|
||||
"Building {} started. Removed assignment for probe {}",
|
||||
unit.get_type().name(),
|
||||
probe_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_stale_commands(player: &Player, state: &mut GameState) {
|
||||
let unit_ids: Vec<usize> = player.get_units().iter().map(|u| u.get_id()).collect();
|
||||
|
||||
state.intended_commands.retain(|unit_id, cmd| {
|
||||
// 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();
|
||||
// When a building is created, remove the corresponding build history entry so the next
|
||||
// build can be started without waiting for the current one to finish.
|
||||
if let Some(pos) = state.unit_build_history.iter().position(|entry| {
|
||||
entry
|
||||
.unit_type
|
||||
.map(|ut| ut == unit.get_type())
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
state.unit_build_history.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
/// Called when a non‑building unit (e.g., a trained unit) is created.
|
||||
/// This clears any pending assignment for the building that trained it.
|
||||
pub fn on_unit_create(_unit: &Unit, _state: &mut GameState) {
|
||||
// No intended command tracking needed for unit creation.
|
||||
}
|
||||
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(builder) = find_builder_for_unit(player, unit_type, state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let builder_id = builder.get_id();
|
||||
|
||||
if assign_builder_to_construct(game, &builder, unit_type, state) {
|
||||
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);
|
||||
|
||||
let current_stage = &state.build_stages[state.current_stage_index];
|
||||
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(
|
||||
_game: &Game,
|
||||
@@ -94,15 +103,12 @@ fn get_status_for_stage_items(
|
||||
state: &GameState,
|
||||
) -> std::collections::HashMap<String, String> {
|
||||
let mut status_map = std::collections::HashMap::new();
|
||||
|
||||
let Some(current_stage) = state.build_stages.get(state.current_stage_index) else {
|
||||
return status_map;
|
||||
};
|
||||
|
||||
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
||||
let unit_name = unit_type.name().to_string();
|
||||
let current_count = count_units_of_type(player, state, *unit_type);
|
||||
|
||||
if current_count >= desired_count {
|
||||
status_map.insert(
|
||||
unit_name,
|
||||
@@ -110,7 +116,6 @@ fn get_status_for_stage_items(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if !can_afford_unit(player, *unit_type) {
|
||||
let minerals_short = unit_type.mineral_price() - player.minerals();
|
||||
let gas_short = unit_type.gas_price() - player.gas();
|
||||
@@ -126,7 +131,6 @@ fn get_status_for_stage_items(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if unit_type.is_building() {
|
||||
if find_builder_for_unit(player, *unit_type, state).is_none() {
|
||||
status_map.insert(
|
||||
@@ -136,79 +140,59 @@ fn get_status_for_stage_items(
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
status_map.insert(
|
||||
unit_name,
|
||||
format!("Ready to build ({}/{})", current_count, desired_count),
|
||||
);
|
||||
}
|
||||
|
||||
status_map
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
if let Some(pylon) = check_need_more_supply(game, player, state) {
|
||||
return Some(pylon);
|
||||
}
|
||||
|
||||
let status_map = get_status_for_stage_items(game, player, state);
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
||||
let current_count = count_units_of_type(player, state, *unit_type);
|
||||
|
||||
if current_count >= desired_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = status_map.get(&unit_type.name().to_string());
|
||||
if status.is_some() && status.unwrap().starts_with("Ready to build") {
|
||||
candidates.push(*unit_type);
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
.into_iter()
|
||||
.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();
|
||||
|
||||
if supply_total == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let supply_remaining = supply_total - supply_used;
|
||||
let threshold = ((supply_total as f32) * 0.15).ceil() as i32;
|
||||
|
||||
if supply_remaining <= threshold && supply_total < 400 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn find_builder_for_unit(
|
||||
player: &Player,
|
||||
unit_type: UnitType,
|
||||
state: &GameState,
|
||||
_state: &GameState,
|
||||
) -> Option<rsbwapi::Unit> {
|
||||
let builder_type = unit_type.what_builds().0;
|
||||
|
||||
player
|
||||
.get_units()
|
||||
.iter()
|
||||
@@ -217,45 +201,37 @@ fn find_builder_for_unit(
|
||||
&& !u.is_constructing()
|
||||
&& !u.is_training()
|
||||
&& (u.is_idle() || u.is_gathering_minerals() || u.is_gathering_gas())
|
||||
&& !state.intended_commands.contains_key(&u.get_id())
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
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!(
|
||||
"Attempting to build {} at {:?} with worker {} (currently at {:?})",
|
||||
"Attempting to build {} at {:?} with worker {}",
|
||||
unit_type.name(),
|
||||
pos,
|
||||
builder_id,
|
||||
builder.get_position()
|
||||
);
|
||||
|
||||
match builder.build(unit_type, pos) {
|
||||
Ok(_) => {
|
||||
println!("Build command succeeded for {}", unit_type.name());
|
||||
let intended_cmd = IntendedCommand {
|
||||
order: Order::PlaceBuilding,
|
||||
target_position: Some(pos.to_position()),
|
||||
target_unit: None,
|
||||
};
|
||||
state.intended_commands.insert(builder_id, intended_cmd);
|
||||
true
|
||||
// No intended command for building actions; the builder (worker) will be tracked via ongoing constructions.
|
||||
Some((true, Some(pos)))
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Build command FAILED for {}: {:?}", unit_type.name(), e);
|
||||
false
|
||||
Some((false, None))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -264,41 +240,33 @@ fn assign_builder_to_construct(
|
||||
unit_type.name(),
|
||||
builder.get_id()
|
||||
);
|
||||
false
|
||||
Some((false, None))
|
||||
}
|
||||
} else {
|
||||
match builder.train(unit_type) {
|
||||
Ok(_) => {
|
||||
let intended_cmd = IntendedCommand {
|
||||
order: Order::Train,
|
||||
target_position: None,
|
||||
target_unit: None,
|
||||
};
|
||||
state.intended_commands.insert(builder_id, intended_cmd);
|
||||
true
|
||||
// No intended command for training; the building will be tracked via ongoing constructions.
|
||||
Some((true, None))
|
||||
}
|
||||
Err(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 {
|
||||
let existing = player
|
||||
player
|
||||
.get_units()
|
||||
.iter()
|
||||
.filter(|u| u.get_type() == unit_type)
|
||||
.count() as i32;
|
||||
|
||||
existing
|
||||
.count() as i32
|
||||
}
|
||||
|
||||
fn can_afford_unit(player: &Player, unit_type: UnitType) -> bool {
|
||||
let minerals = player.minerals();
|
||||
let gas = player.gas();
|
||||
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
|
||||
let stage_complete = current_stage
|
||||
.desired_counts
|
||||
.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);
|
||||
current_count >= desired_count
|
||||
});
|
||||
|
||||
if stage_complete {
|
||||
let next_stage_index = state.current_stage_index + 1;
|
||||
|
||||
if next_stage_index < state.build_stages.len() {
|
||||
println!(
|
||||
"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) {
|
||||
let mut y = 10;
|
||||
let x = 3;
|
||||
|
||||
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_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);
|
||||
y += 10;
|
||||
|
||||
if let Some(last_entry) = state.unit_build_history.last() {
|
||||
let unit_name = if let Some(unit_type) = last_entry.unit_type {
|
||||
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");
|
||||
}
|
||||
y += 10;
|
||||
|
||||
game.draw_text_screen((x, y), "Stage Progress:");
|
||||
y += 10;
|
||||
|
||||
for (unit_type, &desired_count) in ¤t_stage.desired_counts {
|
||||
let current_count = count_units_of_type(player, state, *unit_type);
|
||||
game.draw_text_screen(
|
||||
|
||||
@@ -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) {
|
||||
let all_units = player.get_units();
|
||||
let workers: Vec<Unit> = all_units
|
||||
.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()
|
||||
.collect();
|
||||
|
||||
// Assign idle workers to mining
|
||||
// Assign idle workers that are not already assigned in the build history
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
let worker_id = worker.get_id();
|
||||
|
||||
if let Some(cmd) = state.intended_commands.get(&worker_id) {
|
||||
if cmd.order != Order::MiningMinerals {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !worker.is_idle() {
|
||||
// Skip if the worker is currently assigned to an in‑progress building construction.
|
||||
let has_in_progress_build = state.unit_build_history.iter().any(|entry| {
|
||||
entry.assigned_unit_id == Some(worker_id)
|
||||
&& entry.unit_type.map(|ut| ut.is_building()).unwrap_or(false)
|
||||
});
|
||||
if has_in_progress_build {
|
||||
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;
|
||||
}
|
||||
|
||||
// Find a mineral patch to assign.
|
||||
let Some(mineral) = find_available_mineral(game, worker, state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let intended_cmd = IntendedCommand {
|
||||
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()
|
||||
);
|
||||
|
||||
// Assign worker to gather minerals (ignore intended command tracking).
|
||||
if worker.gather(&mineral).is_ok() {
|
||||
println!(
|
||||
"Assigned worker {} to mine from mineral at {:?}",
|
||||
worker_id,
|
||||
mineral.get_position()
|
||||
);
|
||||
println!("Assigned worker {} to mine from mineral", worker_id,);
|
||||
}
|
||||
}
|
||||
|
||||
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 minerals = game.get_static_minerals();
|
||||
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| {
|
||||
let pos = m.get_position();
|
||||
((pos.x - worker_pos.x).pow(2) + (pos.y - worker_pos.y).pow(2)) as i32
|
||||
});
|
||||
|
||||
for mineral in mineral_list.iter() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
// Return the closest mineral, ignoring any intended command tracking.
|
||||
mineral_list.first().cloned()
|
||||
}
|
||||
|
||||
@@ -31,13 +31,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #d4af37;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -60,13 +53,17 @@
|
||||
font-weight: bold;
|
||||
color: #d4af37;
|
||||
display: block;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.speed-fast {
|
||||
color: #4ade80; /* green */
|
||||
}
|
||||
.speed-slow {
|
||||
color: #f87171; /* red */
|
||||
}
|
||||
|
||||
.speed-label {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
display: none; /* hide label for compact view */
|
||||
}
|
||||
|
||||
.preset-buttons {
|
||||
@@ -87,6 +84,10 @@
|
||||
background: #2d2d2d;
|
||||
color: #d4af37;
|
||||
}
|
||||
button.active {
|
||||
background: #d4af37;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
button small {
|
||||
display: block;
|
||||
@@ -201,27 +202,28 @@
|
||||
</head>
|
||||
<body>
|
||||
<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">
|
||||
<label>Select Speed</label>
|
||||
<div class="preset-buttons">
|
||||
<button onclick="setSpeed(42)">42<br /><small>Slowest</small></button>
|
||||
<button onclick="setSpeed(1)">1<br /><small>Fast</small></button>
|
||||
<button onclick="setSpeed(0)">0<br /><small>Fastest</small></button>
|
||||
<button onclick="setSpeed(-1)">-1<br /><small>Max</small></button>
|
||||
<button data-speed="42" onclick="setSpeed(42)">
|
||||
42<br /><small>Slowest</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 id="status" class="status"></div>
|
||||
|
||||
<div class="build-status-section">
|
||||
<h2>📋 Build Status</h2>
|
||||
<div class="stage-info">
|
||||
<div class="stage-name" id="stageName">Loading...</div>
|
||||
<ul class="build-items" id="buildItems">
|
||||
@@ -232,47 +234,53 @@
|
||||
</div>
|
||||
|
||||
<script id="game-speed-script">
|
||||
const currentSpeedDisplay = document.getElementById("currentSpeed");
|
||||
const statusDiv = document.getElementById("status");
|
||||
const presetButtons = document.querySelectorAll('.preset-buttons button');
|
||||
const statusDiv = document.getElementById('status');
|
||||
|
||||
function highlightButton(speed) {
|
||||
presetButtons.forEach(btn => {
|
||||
if (Number(btn.dataset.speed) === speed) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchCurrentSpeed() {
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:3333/api/speed");
|
||||
const response = await fetch('http://127.0.0.1:3333/api/speed');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
updateSpeedDisplay(data.speed);
|
||||
highlightButton(data.speed);
|
||||
// Clear any previous error message
|
||||
statusDiv.textContent = '';
|
||||
statusDiv.className = 'status';
|
||||
} else {
|
||||
showStatus('Unable to connect to bot', 'error');
|
||||
}
|
||||
} 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",
|
||||
const response = await fetch('http://127.0.0.1:3333/api/speed', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ speed: speed }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
updateSpeedDisplay(data.speed);
|
||||
showStatus(`Speed set to ${data.speed}`, "success");
|
||||
highlightButton(data.speed);
|
||||
// Clear any previous status message on success
|
||||
statusDiv.textContent = '';
|
||||
statusDiv.className = 'status';
|
||||
} else {
|
||||
showStatus("Failed to update speed", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showStatus("Connection error", "error");
|
||||
showStatus('Failed to update speed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showStatus(message, type) {
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.className = `status ${type} visible`;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user