real messages from backend to frontend

This commit is contained in:
2026-01-23 09:10:57 -07:00
parent 30a169ed06
commit c39f95b304
7 changed files with 515 additions and 216 deletions

View File

@@ -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<Mutex<GameState>>,
shared_speed: crate::web_server::SharedGameSpeed,
build_status: crate::web_server::SharedBuildStatus,
}
impl ProtosBot {
pub fn new(game_state: Arc<Mutex<GameState>>, shared_speed: crate::web_server::SharedGameSpeed) -> Self {
Self { game_state, shared_speed }
pub fn new(
game_state: Arc<Mutex<GameState>>,
shared_speed: crate::web_server::SharedGameSpeed,
build_status: crate::web_server::SharedBuildStatus,
) -> Self {
Self {
game_state,
shared_speed,
build_status,
}
}
}

View File

@@ -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(),
)
});
}

View File

@@ -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<BuildStage>,
pub current_stage_index: usize,
pub desired_game_speed: i32,
pub stage_item_status: HashMap<String, String>,
}
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(),
}
}
}

View File

@@ -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<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 &current_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<UnitType> {
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 &current_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

View File

@@ -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<String, String>,
}
#[derive(Clone)]
pub struct SharedBuildStatus {
data: Arc<Mutex<BuildStatusData>>,
}
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<String, String>,
) {
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<SharedGameSpeed>) -> Response {
let speed = state.get();
#[derive(Serialize, Deserialize)]
pub struct BuildStatusResponse {
pub stage_name: String,
pub stage_index: usize,
pub items: Vec<BuildItemStatusInfo>,
}
#[derive(Serialize, Deserialize)]
pub struct BuildItemStatusInfo {
pub unit_name: String,
pub status: String,
}
async fn get_game_speed(State(app_state): State<AppState>) -> Response {
let speed = app_state.game_speed.get();
(StatusCode::OK, Json(GameSpeedResponse { speed })).into_response()
}
async fn set_game_speed(
State(state): State<SharedGameSpeed>,
State(app_state): State<AppState>,
Json(payload): Json<GameSpeedRequest>,
) -> 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<AppState>) -> Response {
let data = app_state.build_status.get();
let items: Vec<BuildItemStatusInfo> = 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";