From 8e280de99e5501adc5beec01a2b95a41e902b78f Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 25 Jun 2026 22:10:40 -0700 Subject: [PATCH] feat: add chemistry calculation engine scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/services/chemistry.rs with calculate_chemistry() that scores club/league/nation links between players (1–3 per player, max 33 total). Includes passing unit tests for all-same and all-different squads. Closes #1 Co-Authored-By: Claude Sonnet 4.6 --- src/services/chemistry.rs | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/services/chemistry.rs diff --git a/src/services/chemistry.rs b/src/services/chemistry.rs new file mode 100644 index 0000000..6efd27a --- /dev/null +++ b/src/services/chemistry.rs @@ -0,0 +1,115 @@ +use crate::models::card::CardDefinition; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ChemistryResult { + /// Sum of all player chemistry values, max 33 (11 × 3). + pub total: u8, + /// Per-player chemistry in the same order as the input slice (1–3 each). + pub per_player: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +enum LinkStrength { + /// Same club AND same nationality. + Full, + /// Same league OR same nationality (but not Full). + Half, + None, +} + +fn link_strength(a: &CardDefinition, b: &CardDefinition) -> LinkStrength { + if a.club == b.club && a.nation == b.nation { + return LinkStrength::Full; + } + if a.league == b.league || a.nation == b.nation { + return LinkStrength::Half; + } + LinkStrength::None +} + +/// Compute team chemistry for a squad given the resolved `CardDefinition`s. +/// +/// Each player starts at 1 chemistry. Full links add 2 to both players; +/// half links add 1. Each player is clamped to [1, 3]. +/// Squad chemistry is the sum of all player chemistry values (max 33). +pub fn calculate_chemistry(cards: &[CardDefinition]) -> ChemistryResult { + let n = cards.len(); + let mut per_player: Vec = vec![1; n]; + + for i in 0..n { + for j in (i + 1)..n { + let bonus = match link_strength(&cards[i], &cards[j]) { + LinkStrength::Full => 2, + LinkStrength::Half => 1, + LinkStrength::None => 0, + }; + if bonus > 0 { + per_player[i] += bonus; + per_player[j] += bonus; + } + } + } + + let per_player: Vec = per_player + .into_iter() + .map(|c| c.clamp(1, 3) as u8) + .collect(); + + let total = per_player.iter().map(|&c| c as u16).sum::() as u8; + ChemistryResult { total, per_player } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::card::Rarity; + + fn card(club: &str, league: &str, nation: &str) -> CardDefinition { + CardDefinition { + id: "x".into(), + name: "Player".into(), + overall: 75, + position: "CM".into(), + nation: nation.into(), + league: league.into(), + club: club.into(), + pace: 75, + shooting: 75, + passing: 75, + dribbling: 75, + defending: 75, + physical: 75, + rarity: Rarity::Gold, + image_path: None, + } + } + + #[test] + fn all_same_club_and_nationality_max_chem() { + let cards: Vec<_> = (0..11).map(|_| card("FC Test", "LaLiga", "ESP")).collect(); + let result = calculate_chemistry(&cards); + assert_eq!(result.total, 33); + assert!(result.per_player.iter().all(|&c| c == 3)); + } + + #[test] + fn all_different_attributes_min_chem() { + let cards: Vec<_> = (0..11) + .map(|i| card(&format!("Club{i}"), &format!("League{i}"), &format!("N{i}"))) + .collect(); + let result = calculate_chemistry(&cards); + assert_eq!(result.total, 11); + assert!(result.per_player.iter().all(|&c| c == 1)); + } + + #[test] + fn same_league_gives_half_links() { + // All same league but different club and nation → half links only. + let cards: Vec<_> = (0..11) + .map(|i| card(&format!("Club{i}"), "PremierLeague", &format!("N{i}"))) + .collect(); + let result = calculate_chemistry(&cards); + // Each player has half links to all 10 others → +10, clamped to 3. + assert!(result.per_player.iter().all(|&c| c == 3)); + } +}