My AI stopped kicking and started thinking.
It now scores every passing option in real time on four things: how far it is (there is an ideal range, and hopeful punts and toe-pokes both score badly), whether the lane is genuinely open (measured geometrically, not guessed), how much space the receiver has, and how much the ball actually progresses up the pitch. It leads runners, too, scoring where they will be. Out of that comes an Expected Success percentage for each option, and it picks the best one. On the debug overlay it is a line to every team-mate, green for good, red for don't.
It also commits now. It plants, turns to face the receiver, and strikes, instead of firing a pass every frame like it was having a panic attack. Real animation, ball leaves on the contact frame.
Then the off-ball players got the same treatment in reverse. Instead of scoring who to pass to, a Movement Evaluator scores where to stand: open space, a clear lane from the carrier, spread from team-mates. Passing triangles emerged on their own. I did not write "make a triangle" anywhere.
The players kept slowly collapsing on top of each other over a match and I could not see why. Then I realised: while a pass is in the air, the ball has no owner. Every team-mate fell into "loose ball, closest man presses it," and the whole team converged. On every single pass.
Fix: a pass of ours in flight still counts as possession. Possession is not "someone is holding the ball." It includes the ball travelling between two of your players. Obvious in hindsight. Cost me an evening.
One more: players now carry an Aggression trait from 0 to 1. High backs himself to dribble past you and shoots from further out. Low plays safe and passes early. Same code, different personalities.
Shipped: pass evaluator · off-ball movement evaluator · aggression trait