DefensiveCoordination.cs

Part of the soccer match simulation. Assigns man-marking targets, calls keeper line shifts, moves defenders into central box coverage, and sends nearby players to chase second balls.

Rough Code
using System;
using System.Linq;

namespace Socer;

public sealed partial class MatchSimulation
{
    public int[] MarkHandoffs=new int[2],KeeperLineCalls=new int[2];
    public int KeeperWallCalls,SecondBallChases;
    public float[] KeeperLineUntil=new float[2],KeeperLineShift=new float[2];
    float nextMarking, nextKeeperDirection;

    void UpdateDefensiveCoordination()
    {
        if(Seconds>=nextMarking)
        {
            nextMarking=Seconds+.32f;
            for(int team=0;team<2;team++)
            {
                int possession=Owner>=0?Players[Owner].Team:LastTouch;
                if(possession==team || RestartRemaining>0 || GoalHoldRemaining>0)
                {foreach(var p in ActivePlayers.Where(p=>p.Team==team))p.AutoMarkId=-1;continue;}
                var defenders=ActivePlayers.Where(p=>p.Team==team && !p.Keeper && p.Id!=chasers[team] &&
                    p.DirectOrder==DirectOrderKind.None && Seconds>=p.FallUntil).ToList();
                var attackers=ActivePlayers.Where(p=>p.Team!=team && !p.Keeper && p.Id!=Owner &&
                    World(p.Position,team).X<77 && (p.Position-Ball).Length<36 &&
                    // The ball presser handles a player by the corner. Box defenders
                    // stay available for a cross, cutback and the far-post runner.
                    !(World(Ball,team).X<36 && World(p.Position,team).X<36 &&
                      Math.Abs(World(p.Position,team).Y-34)>18))
                    .OrderBy(p=>World(p.Position,team).X+Math.Abs(p.Position.Y-34)*.55f-(p.Id==PassReceiver?12:0)).Take(5).ToArray();
                var previous=defenders.ToDictionary(p=>p.Id,p=>p.AutoMarkId);
                foreach(var p in defenders)p.AutoMarkId=-1;
                foreach(var attacker in attackers)
                {
                    // A field call already assigned this receiver; keep other defenders available.
                    if(ActivePlayers.Any(p=>p.Team==team && p.DirectOrderUntil>Seconds &&
                        p.DirectTargetId==attacker.Id && p.DirectOrder is DirectOrderKind.MarkPlayer or DirectOrderKind.PressPlayer))continue;
                    var goal=World(new Point(0,34),team);
                    var anchor=attacker.Position+(goal-attacker.Position).Unit*1.1f;
                    var chosen=defenders.Where(d=>Observed(d,attacker).HasValue).OrderBy(d=>(d.Position-anchor).Length+
                        (previous[d.Id]==attacker.Id?-2.2f:0)+Math.Abs(d.Home.Y-attacker.Home.Y)*.015f).FirstOrDefault();
                    if(chosen==null || (chosen.Position-attacker.Position).Length>25)continue;
                    if(previous[chosen.Id]!=attacker.Id && previous.Values.Contains(attacker.Id))MarkHandoffs[team]++;
                    chosen.AutoMarkId=attacker.Id;defenders.Remove(chosen);
                }
            }
        }
        if(Seconds<nextKeeperDirection || Owner<0)return;
        nextKeeperDirection=Seconds+3.5f;
        for(int team=0;team<2;team++)
        {
            if(Players[Owner].Team==team)continue;
            float danger=World(Ball,team).X;
            if(danger>49)continue;
            var keeper=ActivePlayers.FirstOrDefault(p=>p.Team==team && p.Keeper);
            if(keeper==null)continue;
            KeeperLineShift[team]=danger<28?-2.5f:1.8f;
            KeeperLineUntil[team]=Seconds+3.8f;KeeperLineCalls[team]++;
            Say(keeper,danger<28?"Линия ниже!":"Линия выше!",false);
        }
    }

    Point ApplyDefensiveCoordination(Footballer p,Point target,int possessionTeam)
    {
        if(p.Keeper || p.DirectOrder!=DirectOrderKind.None || WindupPlayer==p.Id || DuelDefender==p.Id)return target;
        if(possessionTeam!=p.Team && BackLine(p) && KeeperLineUntil[p.Team]>Seconds)
            target.X+=Direction(p.Team)*KeeperLineShift[p.Team];
        if(possessionTeam!=p.Team && p.AutoMarkId>=0 && p.Id!=chasers[p.Team])
        {
            var runner=Players[p.AutoMarkId];
            var seen=Observed(p,runner);
            if(runner.InPlay && runner.Id!=Owner && seen.HasValue && (seen.Value-p.Position).Length<26)
            {
                var goal=World(new Point(0,34),p.Team);
                bool dangerous=(seen.Value-goal).Length<32;
                var cover=seen.Value+(goal-seen.Value).Unit*(dangerous?1.05f:1.6f);
                if(CanObserve(p,runner))
                {
                    float awareness=p.Profile.Defence*p.Profile.RoleFamiliarity;
                    cover+=runner.Velocity*(.20f-(1-awareness)*(1-awareness)*.55f);
                }
                p.Action=dangerous?"Закрывает удар подопечного":"Сопровождает открывание";
                p.DecisionReason=runner.Id==PassReceiver?"Пас идёт подопечному · сближается со стороны ворот":"Держит нападающего перед собой и закрывает путь к воротам";
                return dangerous?Clamp(cover):Clamp(target*.12f+cover*.88f);
            }
        }
        return target;
    }

    Point ProtectCentralBox(Footballer p,Point target,int possessionTeam)
    {
        if(possessionTeam==p.Team || p.Keeper || Owner<0 || Players[Owner].Keeper ||
            p.DirectOrder!=DirectOrderKind.None || p.Id==chasers[p.Team] ||
            DuelDefender==p.Id || Seconds<p.FallUntil || p.BlockUntil>Seconds)return target;
        var ball=World(Ball,p.Team);
        if(ball.X>=36)return target;
        if(p.AutoMarkId>=0 && p.AutoMarkId!=Owner && Players[p.AutoMarkId].InPlay)
        {
            var runner=World(Players[p.AutoMarkId].Position,p.Team);
            if(runner.X<34 && Math.Abs(runner.Y-34)<18)return target;
        }
        bool backLine=p.Home.X<27 || p.Home.X<42 && Math.Abs(p.Home.Y-34)>23;
        if(backLine)
        {
            var line=ActivePlayers.Where(q=>q.Team==p.Team && !q.Keeper &&
                (q.Home.X<27 || q.Home.X<42 && Math.Abs(q.Home.Y-34)>23))
                .OrderBy(q=>q.Home.Y).ToArray();
            int slot=Array.FindIndex(line,q=>q.Id==p.Id);
            float span=line.Length>=5?28:22;
            float lane=34-span*.5f+Math.Max(0,slot)*span/Math.Max(1,line.Length-1);
            float ballSide=Math.Clamp((ball.Y-34)*.12f,-2.5f,2.5f);
            var cover=World(new Point(Math.Clamp(ball.X-3.2f,3.5f,24),lane+ballSide),p.Team);
            float urgency=Math.Clamp((36-ball.X)/14,.25f,.9f);
            p.Action="Защищает штрафную";
            return Clamp(target*(1-urgency)+cover*urgency);
        }
        if(AttackUnit(p) && !StrikerRole(p))
        {
            var local=World(target,p.Team);
            local.X=Math.Clamp(local.X,Math.Max(16,ball.X+3),Math.Max(23,ball.X+11));
            local.Y=Math.Clamp(local.Y,17,51);
            return World(local,p.Team);
        }
        return target;
    }

    void PrepareSecondBall(Footballer clearer,Point landing,float duration=2.6f)
    {
        for(int team=0;team<2;team++)
        {
            int slot=0;
            foreach(var p in ActivePlayers.Where(p=>p.Team==team && !p.Keeper && p.Id!=clearer.Id)
                .OrderBy(p=>(p.Position-landing).Length).Take(2))
            {
                p.SecondBallPoint=Clamp(landing+new Point(team==clearer.Team?-2:2,slot==0?-1.5f:1.5f));
                p.SecondBallUntil=Seconds+duration;
                slot++;SecondBallChases++;
            }
        }
    }
    Point ApplySecondBallMovement(Footballer p,Point target)
    {
        if(p.Keeper || Owner>=0 || BallSpeed<3 || p.SecondBallUntil<=Seconds ||
            WindupPlayer==p.Id || DuelDefender==p.Id)return target;
        p.Action="Идёт на второй мяч";
        // The landing point is useful while the ball is high. After a ground
        // parry, keep tracking its actual deceleration instead of camping at an
        // old predicted point while it rolls several metres past the runner.
        return BallHeight<1 && VerticalSpeed<2?LooseBallMeeting(p):p.SecondBallPoint;
    }
}