diff --git a/protossbot/src/bot.rs b/protossbot/src/bot.rs index ea002d8..cc5de18 100644 --- a/protossbot/src/bot.rs +++ b/protossbot/src/bot.rs @@ -48,6 +48,18 @@ impl AiModule for ProtosBot { build_manager::on_frame(game, &player, &mut locked_state); worker_management::assign_idle_workers_to_minerals(game, &player, &mut locked_state); + // Update web server with current build status + let stage_name = locked_state + .build_stages + .get(locked_state.current_stage_index) + .map(|s| s.name.clone()) + .unwrap_or_else(|| "Unknown".to_string()); + self.build_status.update( + stage_name, + locked_state.current_stage_index, + locked_state.stage_item_status.clone(), + ); + build_manager::print_debug_build_status(game, &player, &locked_state); draw_unit_ids(game); } @@ -92,10 +104,19 @@ impl AiModule for ProtosBot { pub struct ProtosBot { game_state: Arc>, shared_speed: crate::web_server::SharedGameSpeed, + build_status: crate::web_server::SharedBuildStatus, } impl ProtosBot { - pub fn new(game_state: Arc>, shared_speed: crate::web_server::SharedGameSpeed) -> Self { - Self { game_state, shared_speed } + pub fn new( + game_state: Arc>, + shared_speed: crate::web_server::SharedGameSpeed, + build_status: crate::web_server::SharedBuildStatus, + ) -> Self { + Self { + game_state, + shared_speed, + build_status, + } } } diff --git a/protossbot/src/main.rs b/protossbot/src/main.rs index 381a8d8..32fb442 100644 --- a/protossbot/src/main.rs +++ b/protossbot/src/main.rs @@ -6,20 +6,31 @@ mod web_server; use bot::ProtosBot; use state::game_state::GameState; use std::sync::{Arc, Mutex}; -use web_server::SharedGameSpeed; +use web_server::{SharedBuildStatus, SharedGameSpeed}; fn main() { println!("Starting RustBot..."); let game_state = Arc::new(Mutex::new(GameState::default())); let shared_speed = SharedGameSpeed::new(42); // Default speed (slowest) + let build_status = SharedBuildStatus::new(); // Start web server in a separate thread let shared_speed_clone = shared_speed.clone(); + let build_status_clone = build_status.clone(); std::thread::spawn(move || { let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(web_server::start_web_server(shared_speed_clone)); + runtime.block_on(web_server::start_web_server( + shared_speed_clone, + build_status_clone, + )); }); - rsbwapi::start(move |_game| ProtosBot::new(game_state.clone(), shared_speed.clone())); + rsbwapi::start(move |_game| { + ProtosBot::new( + game_state.clone(), + shared_speed.clone(), + build_status.clone(), + ) + }); } diff --git a/protossbot/src/state/game_state.rs b/protossbot/src/state/game_state.rs index 784d185..d798bc2 100644 --- a/protossbot/src/state/game_state.rs +++ b/protossbot/src/state/game_state.rs @@ -1,5 +1,5 @@ -use std::collections::HashMap; use rsbwapi::{Order, Position, Unit, UnitType, UpgradeType}; +use std::collections::HashMap; use crate::state::build_stages::BuildStage; @@ -9,9 +9,9 @@ pub struct GameState { pub build_stages: Vec, pub current_stage_index: usize, pub desired_game_speed: i32, + pub stage_item_status: HashMap, } - impl Default for GameState { fn default() -> Self { Self { @@ -20,6 +20,7 @@ impl Default for GameState { build_stages: crate::state::build_stages::get_build_stages(), current_stage_index: 0, desired_game_speed: 20, + stage_item_status: HashMap::new(), } } } diff --git a/protossbot/src/utils/build_manager.rs b/protossbot/src/utils/build_manager.rs index f3f0815..adb1b39 100644 --- a/protossbot/src/utils/build_manager.rs +++ b/protossbot/src/utils/build_manager.rs @@ -7,6 +7,10 @@ use crate::{ pub fn on_frame(game: &Game, player: &Player, state: &mut GameState) { check_and_advance_stage(player, state); + + // Update stage item status + state.stage_item_status = get_status_for_stage_items(game, player, state); + try_start_next_build(game, player, state); } @@ -61,6 +65,74 @@ fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) { } } +fn get_status_for_stage_items( + game: &Game, + player: &Player, + state: &GameState, +) -> std::collections::HashMap { + 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, + format!("Complete ({}/{})", current_count, desired_count), + ); + 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(); + status_map.insert( + unit_name, + format!( + "Need {} minerals, {} gas ({}/{})", + minerals_short.max(0), + gas_short.max(0), + current_count, + desired_count + ), + ); + continue; + } + + if unit_type.is_building() { + if let Some(builder) = find_builder_for_unit(player, *unit_type) { + let build_location = + build_location_utils::find_build_location(game, &builder, *unit_type, 20); + if build_location.is_none() { + status_map.insert( + unit_name, + format!("No build location ({}/{})", current_count, desired_count), + ); + continue; + } + } else { + status_map.insert( + unit_name, + format!("No builder available ({}/{})", current_count, desired_count), + ); + 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 { let current_stage = state.build_stages.get(state.current_stage_index)?; @@ -68,6 +140,7 @@ fn get_next_thing_to_build(game: &Game, player: &Player, state: &GameState) -> O 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 { @@ -77,20 +150,10 @@ fn get_next_thing_to_build(game: &Game, player: &Player, state: &GameState) -> O continue; } - if !can_afford_unit(player, *unit_type) { - 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); } - - if unit_type.is_building() { - let builder = find_builder_for_unit(player, *unit_type)?; - let build_location = - build_location_utils::find_build_location(game, &builder, *unit_type, 20); - if build_location.is_none() { - continue; - } - } - - candidates.push(*unit_type); } candidates diff --git a/protossbot/src/web_server.rs b/protossbot/src/web_server.rs index 1b521f4..1f2fe4d 100644 --- a/protossbot/src/web_server.rs +++ b/protossbot/src/web_server.rs @@ -6,6 +6,7 @@ use axum::{ Json, Router, }; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tower_http::{cors::CorsLayer, services::ServeDir}; @@ -30,6 +31,48 @@ impl SharedGameSpeed { } } +#[derive(Clone, Default)] +pub struct BuildStatusData { + pub stage_name: String, + pub stage_index: usize, + pub item_status: HashMap, +} + +#[derive(Clone)] +pub struct SharedBuildStatus { + data: Arc>, +} + +impl SharedBuildStatus { + pub fn new() -> Self { + Self { + data: Arc::new(Mutex::new(BuildStatusData::default())), + } + } + + pub fn update( + &self, + stage_name: String, + stage_index: usize, + item_status: HashMap, + ) { + let mut data = self.data.lock().unwrap(); + data.stage_name = stage_name; + data.stage_index = stage_index; + data.item_status = item_status; + } + + pub fn get(&self) -> BuildStatusData { + self.data.lock().unwrap().clone() + } +} + +#[derive(Clone)] +struct AppState { + game_speed: SharedGameSpeed, + build_status: SharedBuildStatus, +} + #[derive(Serialize, Deserialize)] pub struct GameSpeedResponse { pub speed: i32, @@ -40,13 +83,26 @@ pub struct GameSpeedRequest { pub speed: i32, } -async fn get_game_speed(State(state): State) -> Response { - let speed = state.get(); +#[derive(Serialize, Deserialize)] +pub struct BuildStatusResponse { + pub stage_name: String, + pub stage_index: usize, + pub items: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct BuildItemStatusInfo { + pub unit_name: String, + pub status: String, +} + +async fn get_game_speed(State(app_state): State) -> Response { + let speed = app_state.game_speed.get(); (StatusCode::OK, Json(GameSpeedResponse { speed })).into_response() } async fn set_game_speed( - State(state): State, + State(app_state): State, Json(payload): Json, ) -> Response { if payload.speed < -1 || payload.speed > 1000 { @@ -57,7 +113,7 @@ async fn set_game_speed( .into_response(); } - state.set(payload.speed); + app_state.game_speed.set(payload.speed); ( StatusCode::OK, @@ -68,17 +124,44 @@ async fn set_game_speed( .into_response() } -pub async fn start_web_server(shared_speed: SharedGameSpeed) { +async fn get_build_status(State(app_state): State) -> Response { + let data = app_state.build_status.get(); + + let items: Vec = data + .item_status + .iter() + .map(|(unit_name, status)| BuildItemStatusInfo { + unit_name: unit_name.clone(), + status: status.clone(), + }) + .collect(); + + let response = BuildStatusResponse { + stage_name: data.stage_name, + stage_index: data.stage_index, + items, + }; + + (StatusCode::OK, Json(response)).into_response() +} + +pub async fn start_web_server(shared_speed: SharedGameSpeed, build_status: SharedBuildStatus) { let static_dir = std::env::current_dir().unwrap().join("static"); let cors = CorsLayer::very_permissive(); + let app_state = AppState { + game_speed: shared_speed, + build_status, + }; + let app = Router::new() .route("/api/speed", get(get_game_speed)) .route("/api/speed", post(set_game_speed)) + .route("/api/build-status", get(get_build_status)) .layer(cors) .fallback_service(ServeDir::new(static_dir)) - .with_state(shared_speed); + .with_state(app_state); let addr = "127.0.0.1:3333"; diff --git a/protossbot/static/index.html b/protossbot/static/index.html index 65638e5..2eeefcc 100644 --- a/protossbot/static/index.html +++ b/protossbot/static/index.html @@ -1,220 +1,345 @@ - + - - - - Protoss Bot - Game Speed Control - - - -
-

⚡ Game Speed Control

+ .status.error { + background: rgba(239, 68, 68, 0.2); + color: #f87171; + border: 1px solid #ef4444; + } -
- 42 - Current Speed -
+ .build-status-section { + margin-top: 30px; + padding-top: 30px; + border-top: 2px solid #333; + } -
- -
- - - - + .build-status-section h2 { + color: #d4af37; + font-size: 20px; + margin-bottom: 20px; + } + + .stage-info { + background: #2d2d2d; + padding: 15px; + border-radius: 8px; + margin-bottom: 15px; + border: 1px solid #444; + } + + .stage-name { + color: #d4af37; + font-size: 16px; + font-weight: bold; + margin-bottom: 10px; + } + + .build-items { + list-style: none; + } + + .build-item { + padding: 8px 10px; + margin: 5px 0; + background: #1e1e1e; + border-radius: 4px; + border-left: 3px solid #d4af37; + display: flex; + justify-content: space-between; + align-items: center; + } + + .build-item.ready { + border-left-color: #22c55e; + } + + .build-item.complete { + border-left-color: #3b82f6; + opacity: 0.7; + } + + .build-item.waiting { + border-left-color: #f59e0b; + } + + .unit-name { + color: #d4af37; + font-weight: 600; + } + + .unit-status { + color: #999; + font-size: 13px; + } + + + +
+

⚡ Game Speed Control

+ +
+ 42 + Current Speed +
+ +
+ +
+ + + + +
+
+ +
+ +
+

📋 Build Status

+
+
Loading...
+
    +
  • Connecting to bot...
  • +
+
-
-
+ - // Poll for speed changes every 2 seconds - setInterval(fetchCurrentSpeed, 2000); - - + + diff --git a/scripts/5-configure-registry.sh b/scripts/5-configure-registry.sh index d21dd19..b737587 100755 --- a/scripts/5-configure-registry.sh +++ b/scripts/5-configure-registry.sh @@ -20,11 +20,6 @@ INSTALL_DIR="$PROJECT_DIR/starcraft" SC_WIN_PATH="$(winepath -w "$INSTALL_DIR" 2>/dev/null || echo "Z:${INSTALL_DIR}" | sed 's/\//\\\\/g')\\\\" -# if wine REG QUERY "HKEY_LOCAL_MACHINE\\SOFTWARE\\Blizzard Entertainment\\Starcraft" /v InstallPath >/dev/null 2>&1; then -# echo "Registry already configured, skipping..." -# exit 0 -# fi - echo "Configuring registry..." # Core registry entries wine REG ADD "HKEY_LOCAL_MACHINE\\SOFTWARE\\Blizzard Entertainment\\Starcraft" \