persisting changes

This commit is contained in:
2026-01-23 16:17:30 -07:00
parent 997cbdb48e
commit dd24f686b3
5 changed files with 115 additions and 204 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

@@ -31,13 +31,19 @@ pub fn get_build_stages() -> Vec<BuildStage> {
.with_unit(UnitType::Terran_Supply_Depot, 2) .with_unit(UnitType::Terran_Supply_Depot, 2)
.with_unit(UnitType::Terran_Barracks, 1) .with_unit(UnitType::Terran_Barracks, 1)
.with_unit(UnitType::Terran_Refinery, 1), .with_unit(UnitType::Terran_Refinery, 1),
// Stage 2: Defense bunker
BuildStage::new("Defense Bunker") BuildStage::new("Defense Bunker")
.with_unit(UnitType::Terran_SCV, 16) .with_unit(UnitType::Terran_SCV, 16)
.with_unit(UnitType::Terran_Supply_Depot, 3) .with_unit(UnitType::Terran_Supply_Depot, 3)
.with_unit(UnitType::Terran_Command_Center, 1) .with_unit(UnitType::Terran_Command_Center, 1)
.with_unit(UnitType::Terran_Barracks, 1) .with_unit(UnitType::Terran_Barracks, 1)
.with_unit(UnitType::Terran_Refinery, 1) .with_unit(UnitType::Terran_Refinery, 1),
.with_unit(UnitType::Terran_Bunker, 2),
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),
] ]
} }

View File

@@ -13,39 +13,53 @@ pub fn on_frame(game: &Game, player: &Player, state: &mut GameState) {
} }
pub fn on_building_create(unit: &Unit, state: &mut GameState) { pub fn on_building_create(unit: &Unit, state: &mut GameState) {
// When a building is created, clear any intended command for the worker that was assigned to build it.
if let Some(entry) = state if let Some(entry) = state
.unit_build_history .unit_build_history
.iter() .iter()
.rev() .rev()
.find(|e| e.unit_type == Some(unit.get_type())) .find(|e| e.unit_type == Some(unit.get_type()))
{ {
if let Some(probe_id) = entry.assigned_unit_id { if let Some(builder_id) = entry.assigned_unit_id {
state.intended_commands.remove(&probe_id); state.intended_commands.remove(&builder_id);
println!( println!(
"Building {} started. Removed assignment for probe {}", "Building {} started. Removed assignment for worker {}",
unit.get_type().name(), unit.get_type().name(),
probe_id builder_id
);
}
}
}
/// Called when a nonbuilding 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) {
// Find the most recent history entry for this unit type.
if let Some(entry) = state
.unit_build_history
.iter()
.rev()
.find(|e| e.unit_type == Some(unit.get_type()))
{
if let Some(builder_id) = entry.assigned_unit_id {
// For training, we didn't store an intended command, but clear just in case.
state.intended_commands.remove(&builder_id);
println!(
"Unit {} created. Cleared assignment for builder {}",
unit.get_type().name(),
builder_id
); );
} }
} }
} }
fn cleanup_stale_commands(player: &Player, state: &mut GameState) { fn cleanup_stale_commands(player: &Player, state: &mut GameState) {
// Retain intended commands as long as the unit still exists.
// The command will be cleared when the building/unit is created (on_building_create) or when the unit dies.
let unit_ids: Vec<usize> = player.get_units().iter().map(|u| u.get_id()).collect(); let unit_ids: Vec<usize> = player.get_units().iter().map(|u| u.get_id()).collect();
state.intended_commands.retain(|unit_id, cmd| { state
if !unit_ids.contains(unit_id) { .intended_commands
return false; .retain(|unit_id, _cmd| unit_ids.contains(unit_id));
}
if let Some(unit) = player.get_units().iter().find(|u| u.get_id() == *unit_id) {
if cmd.order == Order::PlaceBuilding {
return unit.is_constructing() || unit.get_order() == Order::PlaceBuilding;
}
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) {
@@ -71,29 +85,16 @@ fn try_start_next_build(game: &Game, player: &Player, state: &mut GameState) {
}; };
state.unit_build_history.push(entry); state.unit_build_history.push(entry);
let current_stage = &state.build_stages[state.current_stage_index]; 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 { fn should_start_next_build(_game: &Game, player: &Player, state: &mut GameState) -> bool {
!has_pending_assignment(state) // Only start a new build if there are no ongoing constructions or training actions.
!has_ongoing_constructions(state, player)
} }
fn has_pending_assignment(state: &GameState) -> bool { // Removed: pending assignment tracking now relies on actual unit state via has_ongoing_constructions.
state.unit_build_history.iter().any(|entry| {
if let Some(unit_id) = entry.assigned_unit_id {
state.intended_commands.contains_key(&unit_id)
} else {
false
}
})
}
fn has_ongoing_constructions(state: &GameState, player: &Player) -> bool { fn has_ongoing_constructions(state: &GameState, player: &Player) -> bool {
state.unit_build_history.iter().any(|entry| { state.unit_build_history.iter().any(|entry| {
@@ -237,12 +238,7 @@ fn assign_builder_to_construct(
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,
target_position: Some(pos.to_position()),
target_unit: None,
};
state.intended_commands.insert(builder_id, intended_cmd);
Some((true, Some(pos))) Some((true, Some(pos)))
} }
Err(e) => { Err(e) => {
@@ -261,12 +257,7 @@ fn assign_builder_to_construct(
} 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,
target_position: None,
target_unit: None,
};
state.intended_commands.insert(builder_id, intended_cmd);
Some((true, None)) Some((true, None))
} }
Err(e) => { Err(e) => {
@@ -357,91 +348,4 @@ pub fn print_debug_build_status(game: &Game, player: &Player, state: &GameState)
y += 10; y += 10;
} }
} }
let has_pending = state.unit_build_history.iter().any(|entry| {
if let Some(unit_id) = entry.assigned_unit_id {
state.intended_commands.contains_key(&unit_id)
} else {
false
}
});
for entry in &state.unit_build_history {
if let (Some(unit_type), Some(tile_pos), Some(unit_id)) =
(entry.unit_type, entry.tile_position, entry.assigned_unit_id)
{
if state.intended_commands.contains_key(&unit_id) {
let pixel_x = tile_pos.x * 32;
let pixel_y = tile_pos.y * 32;
let width = unit_type.tile_width() * 32;
let height = unit_type.tile_height() * 32;
use rsbwapi::{Color, Position};
let top_left = Position {
x: pixel_x,
y: pixel_y,
};
let top_right = Position {
x: pixel_x + width,
y: pixel_y,
};
let bottom_left = Position {
x: pixel_x,
y: pixel_y + height,
};
let bottom_right = Position {
x: pixel_x + width,
y: pixel_y + height,
};
let color = Color::Yellow;
game.draw_line_map(top_left, top_right, color);
game.draw_line_map(top_right, bottom_right, color);
game.draw_line_map(bottom_right, bottom_left, color);
game.draw_line_map(bottom_left, top_left, color);
let center = Position {
x: pixel_x + width / 2,
y: pixel_y + height / 2,
};
game.draw_text_map(center, &format!("Pending: {}", unit_type.name()));
}
}
}
if has_pending {
use rsbwapi::{Color, Position, TilePosition};
if let Some(nexus) = player
.get_units()
.iter()
.find(|u| u.get_type() == UnitType::Protoss_Nexus)
{
let nexus_tile = nexus.get_tile_position();
let radius = 30;
for dx in -radius..=radius {
for dy in -radius..=radius {
let tile_x = nexus_tile.x + dx;
let tile_y = nexus_tile.y + dy;
if tile_x < 0 || tile_y < 0 {
continue;
}
let tile_pos = TilePosition {
x: tile_x,
y: tile_y,
};
let dist_sq = (dx * dx + dy * dy) as f32;
if dist_sq > (radius * radius) as f32 {
continue;
}
let is_buildable = game.is_buildable(tile_pos);
if !is_buildable {
continue;
}
let has_power = game.has_power(tile_pos, (1, 1));
let pixel_x = tile_x * 32;
let pixel_y = tile_y * 32;
let center = Position {
x: pixel_x + 16,
y: pixel_y + 16,
};
let color = if has_power { Color::Green } else { Color::Blue };
game.draw_circle_map(center, 3, color, true);
}
}
}
}
} }

View File

@@ -45,13 +45,6 @@ fn assign_worker_to_mineral(game: &Game, worker: &Unit, state: &mut GameState) {
state.intended_commands.insert(worker_id, intended_cmd); 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 at {:?}", "Assigned worker {} to mine from mineral at {:?}",

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;