//! Emit the discard-pricing matrix for an entire FIFA 17 corpus, and audit it. //! //! Uses the SHIPPED implementation (`fut::discard::value_for_definition`) rather //! than reimplementing the formula, so the matrix cannot drift from what the //! server actually pays. //! //! ```text //! cargo run -p openfut-adapter-fifa17 --example discard_matrix -- \ //! [--csv out.csv] //! ``` //! //! Prints an audit summary and, with `--csv`, the full per-definition matrix. use std::collections::{BTreeMap, HashMap}; use openfut_adapter_fifa17::fut::discard; use openfut_adapter_fifa17::fut::item::legacy_discard_value; fn main() { let args: Vec = std::env::args().collect(); if args.len() < 3 { eprintln!("usage: discard_matrix [--csv ]"); std::process::exit(2); } let catalog: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&args[1]).expect("read catalog")) .expect("parse catalog"); let cards: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&args[2]).expect("read cards")) .expect("parse cards"); let csv_path = args .iter() .position(|a| a == "--csv") .map(|i| args[i + 1].clone()); // Core's rating per definition id (non-players are 0, which is exactly why // the catalog rating matters). let mut core_rating: HashMap = HashMap::new(); if let Some(arr) = cards.as_array() { for c in arr { let id = c["id"].as_str().unwrap_or_default().to_string(); let r = c["overall"].as_i64().unwrap_or(0).clamp(0, 255) as u8; core_rating.insert(id, r); } } let entries = catalog .get("cards") .and_then(|c| c.as_object()) .expect("catalog has cards{}"); let mut rows: Vec = Vec::new(); rows.push("definition,kind,subtype,cardtype,rareflag,rating_src,rating,level,legacy,recovered,verdict".into()); let mut by_kind: BTreeMap = BTreeMap::new(); // n, declined, legacy, recovered let (mut negatives, mut zero_priced, mut declined_total, mut overflow) = (0usize, 0usize, 0usize, 0usize); let mut boundary_probe_failures = Vec::new(); for (id, e) in entries { let kind = e["kind"].as_str().unwrap_or("player").to_string(); let subtype = e["subtype"].as_i64().unwrap_or(0); let rareflag = e["rareflag"].as_i64().unwrap_or(0); let cat_rating = e["rating"].as_i64().map(|r| r.clamp(0, 255) as u8); let core = *core_rating.get(id).unwrap_or(&0); let cardtype = discard::cardtype_for_subtype(subtype); let recovered = discard::value_for_definition(subtype, rareflag, cat_rating, core); let effective_rating = cat_rating.unwrap_or(core); let level = discard::discard_level(effective_rating); let legacy = legacy_discard_value(core); let verdict = match recovered { None => { declined_total += 1; "DECLINES->legacy" } Some(v) if v < 0 => { negatives += 1; "NEGATIVE" } Some(0) => { zero_priced += 1; "ZERO" } Some(v) if v > 1_000_000 => { overflow += 1; "IMPLAUSIBLE" } Some(_) => "ok", }; let ent = by_kind.entry(kind.clone()).or_insert((0, 0, 0, 0)); ent.0 += 1; ent.2 += legacy; match recovered { Some(v) => ent.3 += v, None => { ent.1 += 1; ent.3 += legacy; // declining means the legacy ladder is what pays } } rows.push(format!( "{id},{kind},{subtype},{cardtype},{rareflag},{},{effective_rating},{level},{legacy},{},{verdict}", if cat_rating.is_some() { "catalog" } else { "core" }, recovered.map(|v| v.to_string()).unwrap_or_else(|| "-".into()), )); } // Rating-boundary audit against the client's own ladder (cmp 0x4b / 0x41). for (rating, want) in [(0u8, 1u8), (64, 1), (65, 2), (74, 2), (75, 3), (99, 3)] { let got = discard::discard_level(rating); if got != want { boundary_probe_failures.push(format!("rating {rating}: level {got}, expected {want}")); } } println!("== DISCARD MATRIX AUDIT =="); println!("definitions : {}", entries.len()); println!("declined -> legacy : {declined_total}"); println!("priced zero : {zero_priced}"); println!("negative : {negatives}"); println!("implausible (>1e6) : {overflow}"); println!( "rating boundaries : {}", if boundary_probe_failures.is_empty() { "OK (1/2/3 at <65 / 65..74 / >=75)".to_string() } else { boundary_probe_failures.join("; ") } ); println!(); println!( "{:<12} {:>6} {:>9} {:>14} {:>14}", "kind", "n", "declined", "legacy", "recovered" ); let (mut tl, mut tr) = (0i64, 0i64); for (kind, (n, dec, legacy, rec)) in &by_kind { println!("{kind:<12} {n:>6} {dec:>9} {legacy:>14} {rec:>14}"); tl += legacy; tr += rec; } println!( "{:<12} {:>6} {:>9} {:>14} {:>14}", "TOTAL", entries.len(), declined_total, tl, tr ); if tl > 0 { println!("ratio recovered/legacy : {:.2}x", tr as f64 / tl as f64); } if let Some(path) = csv_path { std::fs::write(&path, rows.join("\n") + "\n").expect("write csv"); println!("\nwrote {} rows to {path}", rows.len() - 1); } let fatal = negatives + overflow + boundary_probe_failures.len(); if fatal > 0 { eprintln!("\nFAIL: {fatal} fatal finding(s)"); std::process::exit(1); } println!("\nRESULT: OK"); }