feat: Phase 2 — game feel complete
Chemistry & squads: - Chemistry calculation on GET /squad (club/league/nation links, max 100) - Formation validation on POST /squad (exactly 11 starters, exactly 1 GK) Objectives: - Weekly objectives JSON (4 objectives: warrior, goals, dedicated, SBC) - Daily objectives auto-reset at midnight UTC (background task) SBC validation expanded: - max_overall per-player enforcement - required_clubs validation - min_players_from_same_nation validation - min_players_from_same_club validation Market: - GET /market?min_overall=X&position=Y filtering - Expiry cleanup runs before every NPC refresh - NPC market auto-refresh every 24h (background task, runs at startup) Matches: - GET /matches/opponent?difficulty=beginner|professional|world_class|legendary generates a random AI opponent squad from the card pool Statistics: - win_streak and best_win_streak tracking (migration 0002) - GET /statistics/history?limit=N — last N matches with summary stats Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+55
-11
@@ -130,14 +130,23 @@ fn validate_sbc(def: &SbcDefinition, cards: &[CardDefinition]) -> (bool, Vec<Str
|
||||
if let Some(min) = req.min_overall {
|
||||
let avg = cards.iter().map(|c| c.overall as i64).sum::<i64>() / cards.len() as i64;
|
||||
if avg < min as i64 {
|
||||
failures.push(format!("average overall {avg} < required {min}"));
|
||||
failures.push(format!("average overall {avg} < required minimum {min}"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(max) = req.max_overall {
|
||||
if let Some(card) = cards.iter().find(|c| c.overall > max) {
|
||||
failures.push(format!(
|
||||
"'{}' has overall {} which exceeds maximum {max}",
|
||||
card.name, card.overall
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !req.required_leagues.is_empty() {
|
||||
for league in &req.required_leagues {
|
||||
if !cards.iter().any(|c| &c.league == league) {
|
||||
failures.push(format!("need at least one player from league {league}"));
|
||||
failures.push(format!("need at least one player from league '{league}'"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,22 +154,57 @@ fn validate_sbc(def: &SbcDefinition, cards: &[CardDefinition]) -> (bool, Vec<Str
|
||||
if !req.required_nations.is_empty() {
|
||||
for nation in &req.required_nations {
|
||||
if !cards.iter().any(|c| &c.nation == nation) {
|
||||
failures.push(format!("need at least one player from nation {nation}"));
|
||||
failures.push(format!("need at least one player from nation '{nation}'"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !req.required_clubs.is_empty() {
|
||||
for club in &req.required_clubs {
|
||||
if !cards.iter().any(|c| &c.club == club) {
|
||||
failures.push(format!("need at least one player from club '{club}'"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(min_same_league) = req.min_players_from_same_league {
|
||||
let league_counts: std::collections::HashMap<&str, usize> =
|
||||
cards.iter().fold(Default::default(), |mut m, c| {
|
||||
*m.entry(c.league.as_str()).or_default() += 1;
|
||||
m
|
||||
});
|
||||
let max = league_counts.values().copied().max().unwrap_or(0);
|
||||
if max < min_same_league as usize {
|
||||
failures.push(format!("need {min_same_league} players from same league"));
|
||||
let counts = count_by(cards, |c| c.league.as_str());
|
||||
if counts.values().copied().max().unwrap_or(0) < min_same_league as usize {
|
||||
failures.push(format!(
|
||||
"need at least {min_same_league} players from the same league"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(min_same_nation) = req.min_players_from_same_nation {
|
||||
let counts = count_by(cards, |c| c.nation.as_str());
|
||||
if counts.values().copied().max().unwrap_or(0) < min_same_nation as usize {
|
||||
failures.push(format!(
|
||||
"need at least {min_same_nation} players from the same nation"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(min_same_club) = req.min_players_from_same_club {
|
||||
let counts = count_by(cards, |c| c.club.as_str());
|
||||
if counts.values().copied().max().unwrap_or(0) < min_same_club as usize {
|
||||
failures.push(format!(
|
||||
"need at least {min_same_club} players from the same club"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
(failures.is_empty(), failures)
|
||||
}
|
||||
|
||||
fn count_by<'a, F>(cards: &'a [CardDefinition], key: F) -> std::collections::HashMap<&'a str, usize>
|
||||
where
|
||||
F: Fn(&'a CardDefinition) -> &'a str,
|
||||
{
|
||||
cards
|
||||
.iter()
|
||||
.fold(std::collections::HashMap::new(), |mut m, c| {
|
||||
*m.entry(key(c)).or_default() += 1;
|
||||
m
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user