Scoring: position, not overall rating
A card's OVR is a single number averaged across attributes that a given position may not care about. An 86-rated centre back and an 86-rated winger are not interchangeable, and treating them as such is how you end up with a high-rated squad that plays badly.
Every player is therefore scored against the attributes that matter for the slot being filled: pace and dribbling for wide attackers, defending and physical for centre backs, and so on. Attackers pick up an additional bonus for skill moves and weak foot, since both change what a card can actually do in a match.
Chemistry, by the game's own rules
Chemistry is read from the game's real thresholds rather than approximated:
nation, league and club counts, plus the Hero and Icon link rules that make some
cards contribute differently from their nominal nationality or league. The
thresholds live in squadData.json and are versioned with the app, so
a mid-cycle rules change is a data update rather than a rewrite.
Objectives
- Max stats — the highest positional quality available.
- Max chemistry — the strongest links, quality permitting.
- Max card rating — for SBC-style rating targets.
- Balanced — best stats subject to a chemistry floor, which is what most people actually want.
Objectives can also be ordered rather than chosen. A priority of
stats, chem maximizes stats first, then improves chemistry only
among squads that tie on stats — so the second goal never costs you the first.
Why two solving methods
With no chemistry constraint, slot assignment is separable: each player has a score for each slot and the best XI is an exact min-cost max-flow assignment. It is optimal and effectively instant.
Chemistry breaks that. Whether a player contributes to a link depends on who else is in the squad, so the slots stop being independent and exact assignment no longer applies. For those searches the tool warm-starts from the flow solution and runs simulated annealing over swaps, holding any active constraint floors.
The annealer is seeded, so the same request produces the same squad. That matters more than it sounds: an optimizer you cannot reproduce is an optimizer you cannot debug.
Constraints and infeasibility
Count constraints (nation BRA ≥ 4), per-slot constraints
(slot 7 = GER CAM), minimum team rating and minimum chemistry are
all hard. When they cannot be satisfied together from your club, the answer is
which constraint made the problem infeasible — not a squad that quietly ignores
one of them.
Where the work happens
Solves run in a worker pool, off the request thread, with a bounded queue. A long annealing run for one user cannot stall the app for everyone else; when the queue is full, requests are rejected rather than allowed to pile up.
The solver itself is framework-agnostic and has no network calls or external services — it is pure CPU over data already on disk. That keeps it testable against a set of parity cases whose results are checked in, so a refactor that changes an answer gets caught immediately.
Not affiliated with EA
This is a personal project and has no connection to Electronic Arts. Player and club names are trademarks of their respective owners. Back to the overview.