From b10518b1b01c16edcad4e5ce172ba4d8f6a166ab Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 19 Aug 2026 20:12:06 -0400 Subject: [PATCH] fix(features): take the allocation by index, not by label Two enemies of the same chassis produced the same label, so a lookup by name could declare against one machine while the log named the other. The menu now carries the allocation index it came from, and a label carries the target's id so one decision's rows are readable. --- crates/sds-bot/src/main.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/sds-bot/src/main.rs b/crates/sds-bot/src/main.rs index 57e83a6..e490ff0 100644 --- a/crates/sds-bot/src/main.rs +++ b/crates/sds-bot/src/main.rs @@ -352,6 +352,11 @@ impl Bot { // firing is a candidate with a value. let mut menu: Vec<(String, FeatureVector)> = vec![("hold fire".to_string(), FeatureVector::new())]; + // Menu index to allocation index. Not matched by label on the way back: + // two enemies of the same chassis produce the same label, and a lookup + // by name would sometimes declare against the wrong machine while the + // log said otherwise. + let mut from_menu: Vec> = vec![None]; for (index, (label, target_id, shots)) in allocations.iter().enumerate() { let Some(target) = field.iter().copied().find(|u| u.id == *target_id) else { continue; @@ -391,18 +396,16 @@ impl Bot { .features(), ); menu.push((label.clone(), features)); + from_menu.push(Some(index)); } let Some(row) = Decision::score(me.id, &menu, &self.weights) else { return Action::Fire { attacks: vec![] }; }; - // Index zero is "hold fire"; every other index is one behind the - // allocation it came from only if nothing was skipped, so the label is - // matched rather than the index arithmetic trusted. - let taken = row.taken().label.clone(); + let taken = from_menu[row.chosen]; self.training.insert(me.id, row); - let Some((_, target_id, shots)) = allocations.iter().find(|(label, _, _)| *label == taken) - else { + // `None` is "hold fire", which is a decision and is recorded as one. + let Some((_, target_id, shots)) = taken.map(|index| &allocations[index]) else { return Action::Fire { attacks: vec![] }; }; self.declared.push((me.id, *target_id)); @@ -468,7 +471,10 @@ impl Bot { sizes.dedup(); for size in sizes { out.push(( - format!("{size} at {name}"), + // The id is in the label because two enemies of the same + // chassis are common and two identical labels in one + // decision are unreadable. + format!("{size} at {name} #{target}"), target, affordable[..size].to_vec(), )); -- 2.51.2