feat: add chemistry calculation engine scaffold
CI / Build, lint & test (pull_request) Failing after 1m23s
CI / Build, lint & test (pull_request) Failing after 1m23s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<i16> = 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<u8> = per_player
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| c.clamp(1, 3) as u8)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let total = per_player.iter().map(|&c| c as u16).sum::<u16>() 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user