From 7da834de0b3f95cb30e04756781efed0051df42b Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Wed, 21 Jan 2026 22:13:49 -0700 Subject: [PATCH] updates --- flake.nix | 71 ++++- protossbot/.cargo/config.toml | 1 + protossbot/Cargo.toml | 2 +- protossbot/build.sh | 23 -- protossbot/src/bot.rs | 54 ++-- protossbot/src/game_state.rs | 14 - protossbot/src/main.rs | 9 +- protossbot/src/state/build_stages.rs | 46 ++++ protossbot/src/state/game_state.rs | 46 ++++ protossbot/src/state/mod.rs | 2 + protossbot/src/utils/build_location_utils.rs | 53 ++++ protossbot/src/utils/build_manager.rs | 268 +++++++++++++++++++ protossbot/src/utils/mod.rs | 3 + protossbot/src/utils/worker_management.rs | 122 +++++++++ protossbot/web/Cargo.toml | 20 ++ protossbot/web/Cargo.toml.leptos | 20 ++ protossbot/web/README.md | 32 +++ protossbot/web/src/lib.rs | 66 +++++ protossbot/web/src/main.rs | 27 ++ protossbot/web/style/main.css | 62 +++++ run.sh | 60 ----- scripts/4-configure-bwapi.sh | 2 +- scripts/bwapi-preferences.yml | 14 +- 23 files changed, 885 insertions(+), 132 deletions(-) delete mode 100755 protossbot/build.sh delete mode 100644 protossbot/src/game_state.rs create mode 100644 protossbot/src/state/build_stages.rs create mode 100644 protossbot/src/state/game_state.rs create mode 100644 protossbot/src/state/mod.rs create mode 100644 protossbot/src/utils/build_location_utils.rs create mode 100644 protossbot/src/utils/build_manager.rs create mode 100644 protossbot/src/utils/mod.rs create mode 100644 protossbot/src/utils/worker_management.rs create mode 100644 protossbot/web/Cargo.toml create mode 100644 protossbot/web/Cargo.toml.leptos create mode 100644 protossbot/web/README.md create mode 100644 protossbot/web/src/lib.rs create mode 100644 protossbot/web/src/main.rs create mode 100644 protossbot/web/style/main.css delete mode 100755 run.sh diff --git a/flake.nix b/flake.nix index b288e9b..03200b6 100644 --- a/flake.nix +++ b/flake.nix @@ -46,7 +46,7 @@ shellEnv = { CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = "${mingwCC}/bin/x86_64-w64-mingw32-gcc"; - CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS = "-L ${mingwPkgs.windows.pthreads}/lib"; + CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS = "-L ${mingwPkgs.windows.pthreads}/lib -C link-args=-static-libgcc -C link-args=-static-libstdc++"; CC_x86_64_pc_windows_gnu = "${mingwCC}/bin/x86_64-w64-mingw32-gcc"; CXX_x86_64_pc_windows_gnu = "${mingwCC}/bin/x86_64-w64-mingw32-g++"; AR_x86_64_pc_windows_gnu = "${mingwCC}/bin/x86_64-w64-mingw32-ar"; @@ -97,6 +97,68 @@ cargo check --target x86_64-pc-windows-gnu ''; + startScript = pkgs.writeShellScriptBin "start" '' + set -e + + SCRIPT_DIR="$(pwd)" + SCRIPTS_PATH="$SCRIPT_DIR/scripts" + + export WINEPREFIX="$SCRIPT_DIR/.wine" + export WINEARCH=win64 + export DISPLAY=:0 + export WINEDLLOVERRIDES="mscoree,mshtml=" + export WINEDEBUG=-all + # Add MinGW DLLs to Wine's search path + export WINEDLLPATH="${mingwPkgs.windows.pthreads}/bin:${mingwCC.cc}/x86_64-w64-mingw32/lib" + + # Cleanup function to ensure processes are killed on exit + cleanup() { + echo "" + echo "Cleaning up processes..." + if [ -n "$XVFB_PID" ] && kill -0 $XVFB_PID 2>/dev/null; then + echo "Stopping Xvfb..." + kill $XVFB_PID 2>/dev/null || true + fi + if [ -n "$BOT_PID" ] && kill -0 $BOT_PID 2>/dev/null; then + echo "Stopping protossbot..." + kill $BOT_PID 2>/dev/null || true + fi + killall StarCraft.exe 2>/dev/null || true + echo "Cleanup complete." + } + + # Register cleanup function to run on script exit (success or failure) + trap cleanup EXIT + + if [ ! -d "$WINEPREFIX" ]; then + wine wineboot --init + fi + + echo "Starting Xvfb virtual display..." + Xvfb :0 -auth ~/.Xauthority -screen 0 640x480x24 > /dev/null 2>&1 & + XVFB_PID=$! + + cd scripts + ./4-configure-bwapi.sh + cd .. + + echo "Building protossbot..." + build-protossbot-debug + echo "Starting protossbot..." + cd "$SCRIPT_DIR/protossbot" + + RUST_BACKTRACE=1 RUST_BACKTRACE=full wine target/x86_64-pc-windows-gnu/debug/protossbot.exe & + BOT_PID=$! + echo "protossbot started (PID: $BOT_PID)" + + + echo "Launching StarCraft with BWAPI via Chaoslauncher..." + cd "$SCRIPT_DIR/starcraft/BWAPI/Chaoslauncher" + wine Chaoslauncher.exe + + echo "StarCraft closed." + ''; + in { devShells.default = pkgs.mkShell (shellEnv // { @@ -105,6 +167,7 @@ buildDebugScript cleanScript checkScript + startScript # Additional development tools pkgs.cargo-watch @@ -128,6 +191,7 @@ echo " build-protossbot-debug - Build debug version for Windows" echo " check-protossbot - Quick check without building" echo " clean-protossbot - Clean build artifacts" + echo " start - Run the bot with StarCraft" ''; }); @@ -180,6 +244,11 @@ program = "${checkScript}/bin/check-protossbot"; }; + start = { + type = "app"; + program = "${startScript}/bin/start"; + }; + default = self.apps.${system}.build; }; } diff --git a/protossbot/.cargo/config.toml b/protossbot/.cargo/config.toml index d0b561d..ca9ab30 100644 --- a/protossbot/.cargo/config.toml +++ b/protossbot/.cargo/config.toml @@ -8,6 +8,7 @@ target = "x86_64-pc-windows-gnu" # - CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS # Falls back to system linker if not in Nix shell linker = "x86_64-w64-mingw32-gcc" +rustflags = ["-C", "link-args=-static-libgcc -static-libstdc++"] [env] # TARGET environment variable helps bindgen know we're cross-compiling diff --git a/protossbot/Cargo.toml b/protossbot/Cargo.toml index f2e7ad3..8914766 100644 --- a/protossbot/Cargo.toml +++ b/protossbot/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rustbot" +name = "protossbot" version = "0.1.0" edition = "2021" diff --git a/protossbot/build.sh b/protossbot/build.sh deleted file mode 100755 index 9fe4112..0000000 --- a/protossbot/build.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -set -e -# Set up environment for cross-compilation -export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc -export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc -export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++ -export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar - -# Set include path for bindgen - add C++ standard library and GCC include paths -export BINDGEN_EXTRA_CLANG_ARGS="-I/usr/lib/gcc/x86_64-w64-mingw32/13-posix/include/c++ -I/usr/lib/gcc/x86_64-w64-mingw32/13-posix/include/c++/x86_64-w64-mingw32 -I/usr/lib/gcc/x86_64-w64-mingw32/13-posix/include -I/usr/x86_64-w64-mingw32/include" - -# Build for Windows target -echo "Building..." -cargo build --target x86_64-pc-windows-gnu - -# Check if build was successful -if [ -f "target/x86_64-pc-windows-gnu/debug/rustbot.exe" ]; then - echo "Build successful: target/x86_64-pc-windows-gnu/debug/rustbot.exe" -else - echo "Build failed!" - exit 1 -fi diff --git a/protossbot/src/bot.rs b/protossbot/src/bot.rs index 0b24c33..60ef448 100644 --- a/protossbot/src/bot.rs +++ b/protossbot/src/bot.rs @@ -1,10 +1,18 @@ -use std::sync::{Arc, Mutex}; - use rsbwapi::*; +use std::sync::{Arc, Mutex}; +use crate::{state::game_state::GameState, utils::{build_manager, worker_management}}; -use crate::game_state::GameState; +fn draw_unit_ids(game: &Game) { + for unit in game.get_all_units() { + if unit.exists() { + let pos = unit.get_position(); + let unit_id = unit.get_id(); + game.draw_text_map(pos, &format!("{}", unit_id)); + } + } +} -impl AiModule for RustBot { +impl AiModule for ProtosBot { fn on_start(&mut self, game: &Game) { // SAFETY: rsbwapi uses interior mutability (RefCell) for the command queue. // enable_flag only adds a command to the queue. @@ -15,7 +23,6 @@ impl AiModule for RustBot { } println!("Game started on map: {}", game.map_file_name()); - } fn on_frame(&mut self, game: &Game) { @@ -24,6 +31,15 @@ impl AiModule for RustBot { return; }; + let Some(player) = game.self_() else { + return; + }; + + build_manager::on_frame(game, &player, &mut locked_state); + worker_management::assign_idle_workers_to_minerals(game, &player, &mut locked_state); + + build_manager::print_debug_build_status(game, &player, &locked_state); + draw_unit_ids(game); } fn on_unit_create(&mut self, game: &Game, unit: Unit) { @@ -33,19 +49,14 @@ impl AiModule for RustBot { println!("unit created: {:?}", unit.get_type()); } - fn on_unit_morph(&mut self, game: &Game, unit: Unit) { + fn on_unit_morph(&mut self, _game: &Game, _unit: Unit) {} - } - - fn on_unit_destroy(&mut self, _game: &Game, unit: Unit) { - - } - - fn on_unit_complete(&mut self, game: &Game, unit: Unit) { - let Some(player) = game.self_() else { - return; - }; + fn on_unit_destroy(&mut self, _game: &Game, _unit: Unit) {} + fn on_unit_complete(&mut self, _game: &Game, _unit: Unit) { + // let Some(player) = game.self_() else { + // return; + // }; } fn on_end(&mut self, _game: &Game, is_winner: bool) { @@ -56,14 +67,13 @@ impl AiModule for RustBot { } } } -pub struct RustBot { + +pub struct ProtosBot { game_state: Arc>, } -impl RustBot { +impl ProtosBot { pub fn new(game_state: Arc>) -> Self { - Self { - game_state, - } + Self { game_state } } -} \ No newline at end of file +} diff --git a/protossbot/src/game_state.rs b/protossbot/src/game_state.rs deleted file mode 100644 index d988e57..0000000 --- a/protossbot/src/game_state.rs +++ /dev/null @@ -1,14 +0,0 @@ - - -pub struct GameState { - -} - - -impl Default for GameState { - fn default() -> Self { - Self { - - } - } -} \ No newline at end of file diff --git a/protossbot/src/main.rs b/protossbot/src/main.rs index 7cd52ed..10ca8ca 100644 --- a/protossbot/src/main.rs +++ b/protossbot/src/main.rs @@ -1,9 +1,10 @@ mod bot; -pub mod game_state; +mod state; +mod utils; -use bot::RustBot; +use bot::ProtosBot; use std::sync::{Arc, Mutex}; -use game_state::GameState; +use state::game_state::GameState; fn main() { @@ -11,5 +12,5 @@ fn main() { let game_state = Arc::new(Mutex::new(GameState::default())); - rsbwapi::start(move |_game| RustBot::new(game_state.clone() )); + rsbwapi::start(move |_game| ProtosBot::new(game_state.clone() )); } diff --git a/protossbot/src/state/build_stages.rs b/protossbot/src/state/build_stages.rs new file mode 100644 index 0000000..9774b03 --- /dev/null +++ b/protossbot/src/state/build_stages.rs @@ -0,0 +1,46 @@ +use rsbwapi::UnitType; +use std::collections::HashMap; + +#[derive(Clone, Debug)] +pub struct BuildStage { + pub name: String, + pub desired_counts: HashMap, +} + +impl BuildStage { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + desired_counts: HashMap::new(), + } + } + + pub fn with_unit(mut self, unit_type: UnitType, count: i32) -> Self { + self.desired_counts.insert(unit_type, count); + self + } +} + +pub fn get_build_stages() -> Vec { + vec![ + BuildStage::new("Start") + .with_unit(UnitType::Protoss_Probe, 10) + .with_unit(UnitType::Protoss_Pylon, 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), + ] +} diff --git a/protossbot/src/state/game_state.rs b/protossbot/src/state/game_state.rs new file mode 100644 index 0000000..784d185 --- /dev/null +++ b/protossbot/src/state/game_state.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; +use rsbwapi::{Order, Position, Unit, UnitType, UpgradeType}; + +use crate::state::build_stages::BuildStage; + +pub struct GameState { + pub intended_commands: HashMap, + pub unit_build_history: Vec, + pub build_stages: Vec, + pub current_stage_index: usize, + pub desired_game_speed: i32, +} + + +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, + desired_game_speed: 20, + } + } +} + +#[derive(Clone, Debug)] +pub struct IntendedCommand { + pub order: Order, + pub target_position: Option, + pub target_unit: Option, +} + +#[derive(Clone, Debug)] +pub enum BuildStatus { + Assigned, + Started, +} + +#[derive(Clone, Debug)] +pub struct BuildHistoryEntry { + pub unit_type: Option, + pub upgrade_type: Option, + pub assigned_unit_id: Option, + // pub status: BuildStatus, +} diff --git a/protossbot/src/state/mod.rs b/protossbot/src/state/mod.rs new file mode 100644 index 0000000..9df5bf8 --- /dev/null +++ b/protossbot/src/state/mod.rs @@ -0,0 +1,2 @@ +pub mod build_stages; +pub mod game_state; diff --git a/protossbot/src/utils/build_location_utils.rs b/protossbot/src/utils/build_location_utils.rs new file mode 100644 index 0000000..27fc608 --- /dev/null +++ b/protossbot/src/utils/build_location_utils.rs @@ -0,0 +1,53 @@ +use rsbwapi::{Game, TilePosition, Unit, UnitType}; + +pub fn find_build_location( + game: &Game, + builder: &Unit, + building_type: UnitType, + max_range: i32, +) -> Option { + + let start_tile = builder.get_tile_position(); + + 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 is_valid_build_location(game, building_type, tile, builder) { + return Some(tile); + } + } + } + } + + None +} + +fn is_valid_build_location( + game: &Game, + building_type: UnitType, + position: TilePosition, + builder: &Unit, +) -> bool { + if !game.can_build_here(builder, position, building_type, false).unwrap_or(false) { + return false; + } + + // if building_type.requires_psi() && !game.has_power(position, building_type) { + // return false; + // } + + // if building_type.requires_creep() && !game.has_creep(position) { + // return false; + // } + + true +} diff --git a/protossbot/src/utils/build_manager.rs b/protossbot/src/utils/build_manager.rs new file mode 100644 index 0000000..dd1626a --- /dev/null +++ b/protossbot/src/utils/build_manager.rs @@ -0,0 +1,268 @@ +use rsbwapi::{Game, Order, Player, UnitType}; + +use crate::{ + state::game_state::{BuildHistoryEntry, GameState, IntendedCommand}, + utils::build_location_utils, +}; + +pub fn on_frame(game: &Game, player: &Player, state: &mut GameState) { + check_and_advance_stage(player, state); + try_start_next_build(game, player, state); +} + +fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) { + let Some(unit_type) = get_next_thing_to_build(game, player, state) else { + return; + }; + + let Some(builder) = find_builder_for_unit(player, unit_type) else { + return; + }; + + 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), + }; + + 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 get_next_thing_to_build(game: &Game, player: &Player, state: &GameState) -> Option { + let current_stage = state.build_stages.get(state.current_stage_index)?; + + if let Some(pylon) = check_need_more_supply(game, player) { + return Some(pylon); + } + + 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; + } + + if !can_afford_unit(player, *unit_type) { + continue; + } + + 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.into_iter().max_by_key(|unit_type| { + unit_type.mineral_price() + unit_type.gas_price() + }) +} + +fn check_need_more_supply(game: &Game, player: &Player) -> Option { + 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) { + let build_location = build_location_utils::find_build_location(game, &builder, pylon_type, 20); + if build_location.is_some() { + return Some(pylon_type); + } + } + } + } + + None +} + +fn find_builder_for_unit(player: &Player, unit_type: UnitType) -> Option { + let builder_type = unit_type.what_builds().0; + + player + .get_units() + .iter() + .find(|u| { + u.get_type() == builder_type && !u.is_constructing() && !u.is_training() && u.is_idle() + }) + .cloned() +} + +fn assign_builder_to_construct(game: &Game, builder: &rsbwapi::Unit, unit_type: UnitType, state: &mut GameState) -> bool { + let builder_id = builder.get_id(); + + if unit_type.is_building() { + let build_location = build_location_utils::find_build_location(game, builder, unit_type, 20); + + if let Some(pos) = build_location { + println!( + "Attempting to build {} at {:?} with worker {} (currently at {:?})", + 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 + } + Err(e) => { + println!("Build command FAILED for {}: {:?}", unit_type.name(), e); + false + } + } + } else { + println!( + "No valid build location found for {} by builder {}", + unit_type.name(), + builder.get_id() + ); + false + } + } 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 + } + Err(e) => { + println!("Train command FAILED for {}: {:?}", unit_type.name(), e); + false + } + } + } +} + +fn count_units_of_type(player: &Player, _state: &GameState, unit_type: UnitType) -> i32 { + let existing = player + .get_units() + .iter() + .filter(|u| u.get_type() == unit_type) + .count() as i32; + + + + existing +} + +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() +} + +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() + .all(|(unit_type, &desired_count)| { + 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 {}", + current_stage.name, state.build_stages[next_stage_index].name + ); + state.current_stage_index = next_stage_index; + } + } +} + +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 { + format!( + "Next: {} ({}/{} M, {}/{} G)", + unit_type.name(), + player.minerals(), + unit_type.mineral_price(), + player.gas(), + unit_type.gas_price() + ) + } else { + "Next: None".to_string() + }; + 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() + } else if let Some(_upgrade) = last_entry.upgrade_type { + "Upgrade" + } else { + "Unknown" + }; + game.draw_text_screen((x, y), &format!("Last Built: {}", unit_name)); + } else { + 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( + (x + 10, y), + &format!("{}: {}/{}", unit_type.name(), current_count, desired_count), + ); + y += 10; + } + } +} diff --git a/protossbot/src/utils/mod.rs b/protossbot/src/utils/mod.rs new file mode 100644 index 0000000..64eab88 --- /dev/null +++ b/protossbot/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod build_location_utils; +pub mod build_manager; +pub mod worker_management; diff --git a/protossbot/src/utils/worker_management.rs b/protossbot/src/utils/worker_management.rs new file mode 100644 index 0000000..7708a41 --- /dev/null +++ b/protossbot/src/utils/worker_management.rs @@ -0,0 +1,122 @@ +use rsbwapi::{Game, Order, Player, Unit}; + +use crate::state::game_state::{GameState, IntendedCommand}; + +pub fn assign_idle_workers_to_minerals(game: &Game, player: &Player, state: &mut GameState) { + let all_units = player.get_units(); + let workers: Vec = all_units + .iter() + .filter(|u| u.get_type().is_worker() && u.is_completed()) + .cloned() + .collect(); + + // First, clean up workers that finished building + reassign_finished_builders(game, &workers, state); + + // Then assign idle workers to mining + for worker in workers { + assign_worker_to_mineral(game, &worker, state); + } +} + +fn reassign_finished_builders(_game: &Game, workers: &[Unit], state: &mut GameState) { + for worker in workers { + let worker_id = worker.get_id(); + + if let Some(cmd) = state.intended_commands.get(&worker_id) { + // For building commands, only remove if the worker was constructing and now is not + // This prevents premature removal when the worker is still moving to the build site + if cmd.order == Order::PlaceBuilding { + // Worker finished if it WAS constructing but no longer is and is idle + // We check the actual order to see if it's moved on from building + let current_order = worker.get_order(); + if worker.is_idle() && current_order != Order::PlaceBuilding && current_order != Order::ConstructingBuilding { + println!("Worker {} finished building, reassigning to minerals", worker_id); + state.intended_commands.remove(&worker_id); + } + } else if cmd.order == Order::Train && worker.is_idle() && !worker.is_training() { + // For training, the original logic is fine + state.intended_commands.remove(&worker_id); + } + } + } +} + +fn assign_worker_to_mineral(game: &Game, worker: &Unit, state: &mut GameState) { + let worker_id = worker.get_id(); + + // Don't reassign workers that have a non-mining intended command + if let Some(cmd) = state.intended_commands.get(&worker_id) { + if cmd.order != Order::MiningMinerals { + return; + } + } + + // Only assign idle workers + if !worker.is_idle() { + return; + } + + if worker.is_gathering_minerals() || worker.is_gathering_gas() { + return; + } + + 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() + ); + + if worker.gather(&mineral).is_ok() { + println!( + "Assigned worker {} to mine from mineral at {:?}", + worker_id, + mineral.get_position() + ); + } +} + +fn find_available_mineral(game: &Game, worker: &Unit, state: &GameState) -> Option { + let worker_pos = worker.get_position(); + let minerals = game.get_static_minerals(); + let mut mineral_list: Vec = minerals.iter().filter(|m| m.exists()).cloned().collect(); + + 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()); + } + } + mineral_list.first().cloned() +} diff --git a/protossbot/web/Cargo.toml b/protossbot/web/Cargo.toml new file mode 100644 index 0000000..3564c2e --- /dev/null +++ b/protossbot/web/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "protoss-bot-web" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = { version = "0.6", features = ["csr"] } +leptos_axum = { version = "0.6" } +leptos_meta = { version = "0.6" } +leptos_router = { version = "0.6" } +axum = "0.7" +tokio = { version = "1", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["fs"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +lazy_static = "1.4" + +[lib] +crate-type = ["cdylib", "rlib"] diff --git a/protossbot/web/Cargo.toml.leptos b/protossbot/web/Cargo.toml.leptos new file mode 100644 index 0000000..0edfae0 --- /dev/null +++ b/protossbot/web/Cargo.toml.leptos @@ -0,0 +1,20 @@ +# Leptos configuration file +[package] +name = "protoss-bot-web" +version = "0.1.0" + +[leptos] +output-name = "protoss-bot-web" +site-root = "target/site" +site-pkg-dir = "pkg" +style-file = "style/main.css" +assets-dir = "public" +site-addr = "127.0.0.1:3000" +reload-port = 3001 +browserquery = "defaults" +watch = false +env = "DEV" +bin-features = ["ssr"] +bin-default-features = false +lib-features = ["hydrate"] +lib-default-features = false diff --git a/protossbot/web/README.md b/protossbot/web/README.md new file mode 100644 index 0000000..f10d4cf --- /dev/null +++ b/protossbot/web/README.md @@ -0,0 +1,32 @@ +# Protoss Bot Web Control Panel + +A Leptos web interface to control the Protoss bot in real-time. + +## Setup + +1. Install cargo-leptos: +```bash +cargo install cargo-leptos +``` + +2. Run the development server: +```bash +cd web +cargo leptos watch +``` + +3. Open your browser to `http://localhost:3000` + +## Features + +- **Game Speed Control**: Set the desired game speed with convenient buttons +- Real-time updates via server functions +- Dark theme matching StarCraft aesthetics + +## Integration with Bot + +The web server exposes a global `GAME_SPEED` variable that can be read by the bot. To integrate: + +1. Add the web crate as a dependency in your bot's `Cargo.toml` +2. Read the speed value: `protoss_bot_web::GAME_SPEED.read().unwrap()` +3. Apply it to the game using BWAPI's setLocalSpeed() or setFrameSkip() diff --git a/protossbot/web/src/lib.rs b/protossbot/web/src/lib.rs new file mode 100644 index 0000000..599789a --- /dev/null +++ b/protossbot/web/src/lib.rs @@ -0,0 +1,66 @@ +use leptos::*; +use leptos_meta::*; +use leptos_router::*; +use std::sync::{Arc, RwLock}; + +// Global static for game speed (shared with the bot) +lazy_static::lazy_static! { + pub static ref GAME_SPEED: Arc> = Arc::new(RwLock::new(20)); +} + +#[component] +pub fn App() -> impl IntoView { + provide_meta_context(); + + view! { + + + <Router> + <main> + <Routes> + <Route path="" view=HomePage/> + </Routes> + </main> + </Router> + } +} + +#[component] +fn HomePage() -> impl IntoView { + let (game_speed, set_game_speed) = create_signal(20); + + let set_speed = move |speed: i32| { + set_game_speed.set(speed); + spawn_local(async move { + let _ = set_game_speed_server(speed).await; + }); + }; + + view! { + <div class="container"> + <h1>"Protoss Bot Control Panel"</h1> + + <div class="speed-control"> + <h2>"Game Speed Control"</h2> + <p>"Current Speed: " {game_speed}</p> + + <div class="button-group"> + <button on:click=move |_| set_speed(0)>"Slowest (0)"</button> + <button on:click=move |_| set_speed(10)>"Slower (10)"</button> + <button on:click=move |_| set_speed(20)>"Normal (20)"</button> + <button on:click=move |_| set_speed(30)>"Fast (30)"</button> + <button on:click=move |_| set_speed(42)>"Fastest (42)"</button> + </div> + </div> + </div> + } +} + +#[server(SetGameSpeed, "/api")] +pub async fn set_game_speed_server(speed: i32) -> Result<(), ServerFnError> { + if let Ok(mut game_speed) = GAME_SPEED.write() { + *game_speed = speed; + println!("Game speed set to: {}", speed); + } + Ok(()) +} diff --git a/protossbot/web/src/main.rs b/protossbot/web/src/main.rs new file mode 100644 index 0000000..4c57fa0 --- /dev/null +++ b/protossbot/web/src/main.rs @@ -0,0 +1,27 @@ +use axum::{ + routing::get, + Router, +}; +use leptos::*; +use leptos_axum::{generate_route_list, LeptosRoutes}; +use protoss_bot_web::App; +use tower_http::services::ServeDir; + +#[tokio::main] +async fn main() { + let conf = get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(App); + + let app = Router::new() + .leptos_routes(&leptos_options, routes, App) + .fallback(leptos_axum::file_and_error_handler(App)) + .with_state(leptos_options); + + println!("Listening on http://{}", &addr); + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); +} diff --git a/protossbot/web/style/main.css b/protossbot/web/style/main.css new file mode 100644 index 0000000..04d9197 --- /dev/null +++ b/protossbot/web/style/main.css @@ -0,0 +1,62 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + margin: 0; + padding: 20px; + background-color: #1a1a1a; + color: #ffffff; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +h1 { + color: #4a9eff; + margin-bottom: 30px; +} + +h2 { + color: #ffffff; + margin-bottom: 15px; +} + +.speed-control { + background-color: #2a2a2a; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; +} + +.button-group { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 15px; +} + +button { + background-color: #4a9eff; + color: white; + border: none; + padding: 12px 24px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background-color 0.2s; +} + +button:hover { + background-color: #357abd; +} + +button:active { + background-color: #2a5f9a; +} + +p { + color: #cccccc; + margin: 10px 0; +} diff --git a/run.sh b/run.sh deleted file mode 100755 index f70af29..0000000 --- a/run.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SCRIPTS_PATH="${SCRIPT_DIR}/scripts" - -export WINEPREFIX="$SCRIPT_DIR/.wine" -export WINEARCH=win64 -export DISPLAY=:0 -export WINEDLLOVERRIDES="mscoree,mshtml=" -export WINEDEBUG=-all - -# Cleanup function to ensure processes are killed on exit -cleanup() { - echo "" - echo "Cleaning up processes..." - if [ -n "$XVFB_PID" ] && kill -0 $XVFB_PID 2>/dev/null; then - echo "Stopping Xvfb..." - kill $XVFB_PID 2>/dev/null || true - fi - if [ -n "$BOT_PID" ] && kill -0 $BOT_PID 2>/dev/null; then - echo "Stopping protossbot..." - kill $BOT_PID 2>/dev/null || true - fi - killall StarCraft.exe - echo "Cleanup complete." -} - -# Register cleanup function to run on script exit (success or failure) -trap cleanup EXIT - -if [ ! -d "$WINEPREFIX" ]; then - wine wineboot --init -fi - -echo "Starting Xvfb virtual display..." -Xvfb :0 -auth ~/.Xauthority -screen 0 640x480x24 > /dev/null 2>&1 & -XVFB_PID=$! - -cd scripts - ./4-configure-bwapi.sh -cd .. - -echo "Building protossbot..." -# build-protossbot-debug -nix develop -c build-protossbot-debug -echo "Starting protossbot..." -cd "$SCRIPT_DIR/protossbot" - -RUST_BACKTRACE=1 RUST_BACKTRACE=full wine target/x86_64-pc-windows-gnu/debug/protossbot.exe & -BOT_PID=$! -echo "protossbot started (PID: $BOT_PID)" - - -echo "Launching StarCraft with BWAPI via Chaoslauncher..." -cd "$SCRIPT_DIR/starcraft/BWAPI/Chaoslauncher" -wine Chaoslauncher.exe - -echo "StarCraft closed." diff --git a/scripts/4-configure-bwapi.sh b/scripts/4-configure-bwapi.sh index d23e24a..5f0c45a 100755 --- a/scripts/4-configure-bwapi.sh +++ b/scripts/4-configure-bwapi.sh @@ -134,7 +134,7 @@ auto_menu = $AUTO_MENU ; if FIRST (default), use the first character in the list ; if WAIT, stop at this screen ; else the character with the given value is used/created -character_name = FIRST +character_name = ProtossPest ; pause_dbg = ON | OFF ; This specifies if auto_menu will pause until a debugger is attached to the process. diff --git a/scripts/bwapi-preferences.yml b/scripts/bwapi-preferences.yml index 985b445..e4d3a40 100644 --- a/scripts/bwapi-preferences.yml +++ b/scripts/bwapi-preferences.yml @@ -3,17 +3,18 @@ auto_menu: SINGLE_PLAYER # auto_menu: LAN # auto_menu: BATTLE_NET -map: maps/BroodWar/(2)Benzene.scx +map: maps/BroodWar/(4)CircuitBreaker.scx +# map: maps/BroodWar/(2)Benzene.scx # map: maps/BroodWar/IceHunter.scm # map: maps/(2)Boxer.scm -player_race: Zerg +# player_race: Zerg # player_race: Terran -# player_race: Protoss +player_race: Protoss # player_race: Random -enemy_count: 1 -enemy_count: 2 +# enemy_count: 1 +enemy_count: 3 computer_races: - Random @@ -24,7 +25,8 @@ computer_races: # - Default # - Default -game_type: MELEE +game_type: FREE_FOR_ALL +# game_type: MELEE # game_type: TEAM_MELEE auto_restart: OFF