Fix multi-copy car handling: track all configs per guid and add copy count

- Change owned_configs from HashMap<String, String> to HashMap<String, Vec<String>>
  to capture all configs across copies (previously last-write-wins lost data)
- Add copy_count field to MyCarsData and CarCounts for wishlist copy-coverage logic
- Fix improve.csv to exclude already-owned final configs as targets (e.g., Fiat
  Campagnola 1974 now targets 332 instead of already-owned 323)
- Fix wishlist to check all owned configs for exact-match and final-build detection
- Fix trash to check all configs for final-build-not-best detection
- Update all tests for new Vec type and add multi-copy improve test
This commit is contained in:
Jan-Peter von Hunnius 2026-06-06 21:13:47 +02:00
parent b24b274d25
commit 62688926c3
7 changed files with 494 additions and 83 deletions

View file

@ -21,10 +21,15 @@ pub struct MyCarsData {
pub owned: HashSet<String>,
/// GUIDs of wishlisted cars (from `wishlists[0].cards`)
pub wishlist: HashSet<String>,
/// Actual tune config for each owned car (guid → "{engineMajor}{weightMajor}{chassisMajor}").
/// Only populated for locked cards. Used by the improve command to determine
/// which cars are not yet at a final build.
pub owned_configs: HashMap<String, String>,
/// Actual tune configs for each owned car (guid → list of configs from all copies).
/// Multiple copies of the same car model share the same guid, so this Vec
/// captures all their individual configs. Previously a single String lost
/// data from all but the last-processed copy.
pub owned_configs: HashMap<String, Vec<String>>,
/// Number of copies owned for each car GUID.
/// Multiple copies of the same car model share the same guid.
/// `owned` HashSet deduplicates, so this counts duplicates.
pub copy_count: HashMap<String, usize>,
}
/// Loads and parses `my_cars.json` once, extracting both owned and wishlist data.
@ -55,13 +60,15 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
// Extract owned car GUIDs and their actual configs from playerDeck
let mut owned: HashSet<String> = HashSet::new();
let mut owned_configs: HashMap<String, String> = HashMap::new();
let mut owned_configs: HashMap<String, Vec<String>> = HashMap::new();
let mut copy_count: HashMap<String, usize> = HashMap::new();
if let Some(deck) = json.get("playerDeck").and_then(|d| d.as_array()) {
for card in deck {
if card.get("locked").and_then(|v| v.as_bool()).unwrap_or(false) {
if let Some(guid) = card.get("cardId").and_then(|v| v.as_str()) {
owned.insert(guid.to_string());
*copy_count.entry(guid.to_string()).or_insert(0) += 1;
// Extract actual tune config: engineMajor + weightMajor + chassisMajor.
// Skip cards with incomplete tune data to avoid false "000" entries
// that would incorrectly appear as needing upgrade.
@ -70,7 +77,7 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
let chassis = card.get("chassisMajor").and_then(|v| v.as_u64());
if let (Some(e), Some(w), Some(c)) = (engine, weight, chassis) {
let config = format!("{e}{w}{c}");
owned_configs.insert(guid.to_string(), config);
owned_configs.entry(guid.to_string()).or_default().push(config);
} else {
eprintln!(
"Warning: locked card {guid} has incomplete tune data — skipped"
@ -90,7 +97,7 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
.map(|cards| cards.iter().filter_map(|c| c.as_str().map(|s| s.to_string())).collect())
.unwrap_or_default();
Ok(MyCarsData { owned, wishlist, owned_configs })
Ok(MyCarsData { owned, wishlist, owned_configs, copy_count })
}
#[cfg(test)]