Add protect/storage lists, dynamic trash thresholds, tie resolution

Protect & storage lists:
- protect.csv: unlocked garage cars (state != 0, locked == false) worth
  locking more copies of. Needed copies = best configs surviving the
  RQ-band thresholds (prize cars and RQ >= 80 always count); already
  locked copies (copy_count) are subtracted, surplus drops off the list.
- storage.csv: Keep/Trash recommendation for storage cars (state == 0)
  using the same copy-count logic; surplus unlocked copies are fusion
  material.
- user_data.rs now parses card state (garage vs storage), garage slots
  (user.garageSlots/garageSlotsUsed), and unlocked card maps.

Dynamic trash thresholds:
- TrashThresholds derive RQ 20-29/30-39 as half/a quarter of a base
  value (--trash-base-count, default 4); RQ 40+ never covered.
- plan_trash_rules escalates the base automatically while projected free
  garage slots (capacity - used + unlocked non-best copies + new trash
  candidates, copies counted per card) stay below --garage-min-free
  (default 20), stopping when no further cars qualify. Effective
  thresholds and projection are printed.
- Improve-based low-value cars (best target count at or below the band
  threshold) land in trash.csv; collect_improve_entries is shared with
  improve.csv and computed once per run.

Same-car tie resolution:
- resolve_tied_configs collapses tie-equivalent configs of the same car
  per track (compute_car_counts, write_details_csv), preferring the
  player's owned final-build config. Fixes phantom rows like the
  Mercedes 280 SL 332 entry tied at 61.31 by its owned 323, inflated
  per-config track counts, and pointless second-copy recommendations.

Wishlist:
- Prize cars are excluded from the ideal wishlist and surface in
  remove.csv via the ideal-set difference.

CLI: new protect command; --trash-base-count/--garage-min-free on
trash/protect/all; details command reuses counts for tie resolution.
README updated for all new files, flags, and criteria.
This commit is contained in:
Jan-Peter von Hunnius 2026-08-21 18:45:37 +02:00
parent ec8be9d138
commit 293a8d7707
12 changed files with 1684 additions and 120 deletions

View file

@ -511,6 +511,44 @@ mod tests {
assert_eq!(records.len(), 1, "Same car_rid should not appear twice in tie list");
}
#[test]
fn test_update_best_car_same_car_different_config_slower_not_added() {
// A slower config of the same car is simply discarded (existing rule).
let ctx = AnalysisContext::new();
let mut record_323 = make_record("Car_A", 85, 100.0, "guid_a");
record_323.normalized_config = "323".to_string();
let mut record_332 = make_record("Car_A", 85, 100.4, "guid_a");
record_332.normalized_config = "332".to_string();
update_best_car(&ctx.best_cars, "laguna", 85, "Dry", record_323);
update_best_car(&ctx.best_cars, "laguna", 85, "Dry", record_332);
let records = ctx.best_cars.get(&("laguna".to_string(), 85, "Dry".to_string())).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].normalized_config, "323");
}
#[test]
fn test_update_best_car_same_car_different_config_faster_replaces() {
// A strictly faster config of the same car replaces the entry.
let ctx = AnalysisContext::new();
let mut record_323 = make_record("Car_A", 85, 100.0, "guid_a");
record_323.normalized_config = "323".to_string();
let mut record_332 = make_record("Car_A", 85, 99.0, "guid_a");
record_332.normalized_config = "332".to_string();
update_best_car(&ctx.best_cars, "laguna", 85, "Dry", record_323);
update_best_car(&ctx.best_cars, "laguna", 85, "Dry", record_332);
let records = ctx.best_cars.get(&("laguna".to_string(), 85, "Dry".to_string())).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].normalized_config, "332", "Faster config replaces the slower one");
}
// =======================================================================
// parse_car_times tests
// =======================================================================