FootballLaboratory.cs

Football laboratory setup for MatchSimulation. Defines drill scenarios (duels, ball control, attack, set pieces, skill moves) and builds a fixed simulation with placed players and attacker/defender ratings.

Rough Code
using System;
using System.Linq;

namespace Socer;

public static class FootballLabScenarios
{
    static (string Id,string Name,string Group)[] Core=>new (string Id,string Name,string Group)[]
    {
        ("dribble","Open 1v1","DUELS"),
        ("nutmeg","Nutmeg","DUELS"),
        ("tackle","Win the ball","DUELS"),
        ("tackle-front","Front poke","DUELS"),
        ("tackle-side","Side hook","DUELS"),
        ("tackle-recovery","Recovery block","DUELS"),
        ("tackle-slide","Sliding tackle","DUELS"),
        ("shoulder","Shoulder challenge","DUELS"),
        ("sprint","Carry at speed","BALL CONTROL"),
        ("first-touch","First touch","BALL CONTROL"),
        ("pass","Pass and receive","BALL CONTROL"),
        ("cross","Wing cross","ATTACK"),
        ("one-on-one","One on one with keeper","ATTACK"),
        ("shot","Shot from distance","ATTACK"),
        ("free-kick","Free kick","SET PIECES"),
        ("corner","Corner","SET PIECES"),
        ("penalty","Penalty","SET PIECES"),
        ("match","Full AI match","MATCH")
    };
    public static (string Id,string Name,string Group)[] All=>Core.Concat(
        SkillMoves.All.Where(s=>s.Kind!=SkillMoveKind.Nutmeg)
            .Select(s=>(Id:$"skill-{s.Kind}",Name:s.Name,Group:"SKILL MOVES"))).ToArray();
    public static bool Contains(string id)=>Array.Exists(All,s=>s.Id==id);
    public static bool TrySkill(string id,out SkillMoveSpec spec)
    {
        spec=SkillMoves.All.FirstOrDefault(s=>id==$"skill-{s.Kind}");
        return spec!=null;
    }
}

public sealed partial class MatchSimulation
{
    public static MatchSimulation Laboratory(string kind,int seed,int attackerRating,int defenderRating)
    {
        if(!FootballLabScenarios.Contains(kind))kind="dribble";
        var m=new MatchSimulation(seed);
        m.HomeName="LAB ATTACK";m.AwayName="LAB DEFENCE";
        foreach(var p in m.Players)p.Profile.Name=p.Team==0?$"Lab attacker #{p.Number}":$"Lab defender #{p.Number}";
        if(kind=="match")
        {
            foreach(var p in m.Players)TuneLabPlayer(p,p.Team==0?attackerRating:defenderRating);
            return m;
        }
        m.Seconds=30;m.RestartRemaining=0;m.Owner=-1;m.Passer=-1;m.LastTouch=0;
        foreach(var p in m.Players)
        {
            p.Position=new Point(p.Team==0?12:93,5+(p.Number%10)*5.6f);
            p.Velocity=new Point();p.Pace=.01f;p.Decision=100;
            p.Recovery=0;p.TackleCooldown=0;
            TuneLabPlayer(p,p.Team==0?attackerRating:defenderRating);
        }
        var runner=m.Players[9];var defender=m.Players[13];var keeper=m.Players[11];
        runner.Held=2;runner.Decision=0;defender.Decision=0;
        runner.Facing=0;defender.Facing=MathF.PI;
        keeper.Position=new Point(103,34);keeper.Pace=5.3f;keeper.Decision=0;
        if(FootballLabScenarios.TrySkill(kind,out var skill))
        {
            runner.Position=new Point(62+(m.Next()-.5f)*1.0f,34+(m.Next()-.5f)*2.0f);
            runner.Velocity=new Point(1.6f+m.Next()*.5f,0);
            defender.Position=new Point(runner.Position.X+2.7f+(m.Next()-.5f)*.6f,
                runner.Position.Y+(m.Next()-.5f)*1.3f);
            int side=defender.Position.Y>=runner.Position.Y?-1:1;
            var exit=new Point(.68f,side*.74f).Unit;
            m.GiveLabBall(runner);
            if(skill.Kind==SkillMoveKind.Scissors)
            {
                for(int attempt=0;attempt<20 && runner.FeintVariant!=3;attempt++)
                {runner.FeintReadyAt=0;m.BeginSkillMove(runner,defender,new Point(1,0),side,exit,skill);}
            }
            else m.BeginSkillMove(runner,defender,new Point(1,0),side,exit,skill);
        }
        else switch(kind)
        {
            case "dribble":
                float lane=(m.Next()-.5f)*7f;
                runner.Position=new Point(60.5f+m.Next()*2.5f,34+lane);
                runner.Velocity=new Point(2.9f+m.Next()*1.1f,(m.Next()-.5f)*.5f);
                defender.Position=new Point(runner.Position.X+3.2f+m.Next()*2.8f,
                    runner.Position.Y+(m.Next()-.5f)*3.2f);
                defender.Velocity=new Point(-.65f+m.Next()*1.1f,(m.Next()-.5f)*.9f);
                m.GiveLabBall(runner);break;
            case "nutmeg":
                runner.Position=new Point(62,34+(m.Next()-.5f)*1.2f);
                defender.Position=new Point(64.25f+(m.Next()-.5f)*.3f,
                    runner.Position.Y+(m.Next()-.5f)*.22f);
                runner.Velocity=new Point(1.9f,0);
                m.GiveLabBall(runner);
                m.BeginSkillMove(runner,defender,new Point(1,0),1,new Point(.72f,.68f).Unit,SkillMoves.Get(SkillMoveKind.Nutmeg));
                break;
            case "tackle":
            case "tackle-front":
                runner.Position=new Point(70,34);runner.Velocity=new Point(2.4f,0);
                defender.Position=new Point(71.35f,34);defender.Velocity=new Point(-1.5f,0);
                m.GiveLabBall(runner);defender.Decision=100;
                m.BeginChallenge(defender,runner);break;
            case "tackle-side":
                runner.Position=new Point(70,34);runner.Velocity=new Point(2.4f,0);
                defender.Position=new Point(70,35.25f);defender.Velocity=new Point(0,-1.5f);
                m.GiveLabBall(runner);defender.Decision=100;
                m.BeginChallenge(defender,runner);break;
            case "tackle-recovery":
                runner.Position=new Point(70,34);runner.Velocity=new Point(2.4f,0);
                defender.Position=new Point(68.65f,34);defender.Velocity=new Point(3.2f,0);
                m.GiveLabBall(runner);defender.Decision=100;
                m.BeginChallenge(defender,runner);break;
            case "tackle-slide":
                runner.Position=new Point(83,34);runner.Velocity=new Point(3.6f,0);
                defender.Position=new Point(81.9f,34);defender.Velocity=new Point(4,0);
                defender.Profile.Aggression=.9f;defender.Stamina=1;
                m.GiveLabBall(runner);defender.Decision=100;
                m.BeginChallenge(defender,runner);break;
            case "shoulder":
                runner.Position=new Point(70,34);runner.Velocity=new Point(2.8f,0);
                defender.Position=new Point(69.9f,34+(m.Next()<.5f?-.72f:.72f));
                defender.Velocity=new Point(2.5f,0);
                m.GiveLabBall(runner);defender.Decision=100;
                m.BeginChallenge(defender,runner);break;
            case "sprint":
                runner.Position=new Point(48,34);runner.Velocity=new Point(4.8f,0);
                runner.RunUntil=38;runner.RunTarget=new Point(93,34);
                defender.Position=new Point(30,60);defender.Pace=.01f;
                m.GiveLabBall(runner);break;
            case "first-touch":
                var passer=m.Players[7];passer.Position=new Point(49,34);
                runner.Position=new Point(62,33);runner.Velocity=new Point(1.8f,0);
                m.Ball=passer.Position+new Point(.3f,0);m.Owner=passer.Id;m.Passer=-1;
                m.Kick(passer,runner.Position+new Point(1.2f,0),16,false);
                m.PassReceiver=runner.Id;m.Passes[0]++;break;
            case "pass":
                var feeder=m.Players[7];feeder.Position=new Point(52,34);
                runner.Position=new Point(70,29);runner.Velocity=new Point(2.4f,0);
                m.Ball=feeder.Position+new Point(.3f,0);m.Owner=feeder.Id;
                m.Kick(feeder,runner.Position+new Point(2,0),20,false);
                m.PassReceiver=runner.Id;m.Passes[0]++;break;
            case "cross":
                var winger=m.Players[10];winger.Position=new Point(87,10);winger.Facing=.2f;
                runner.Position=new Point(94,32);runner.Pace=5.4f;
                defender.Position=new Point(93,34);defender.Pace=5.2f;
                m.Ball=winger.Position+new Point(.3f,0);m.Owner=winger.Id;
                var crossTarget=new Point(96,33);
                m.Kick(winger,crossTarget,20,false);
                m.ShapeFlight(crossTarget,StrikeKind.Cross,1.75f);
                m.PassReceiver=runner.Id;m.Passes[0]++;m.Crosses++;break;
            case "one-on-one":
                runner.Position=new Point(84,34);runner.Velocity=new Point(3.8f,0);
                defender.Position=new Point(79,38);defender.Pace=.01f;
                m.GiveLabBall(runner);break;
            case "shot":
                runner.Position=new Point(75,32);runner.Velocity=new Point(2.2f,0);
                defender.Position=new Point(67,42);defender.Pace=.01f;
                m.GiveLabBall(runner);runner.Decision=0;break;
            case "free-kick":
                m.PrepareLabSetPiece();m.Restart(0,new Point(78,34),"Free kick",kind:RestartKind.FreeKick);break;
            case "corner":
                m.PrepareLabSetPiece();m.Restart(0,new Point(104.3f,.7f),"Corner",kind:RestartKind.Corner);break;
            case "penalty":
                m.PrepareLabSetPiece();m.Restart(0,new Point(94,34),"Penalty",kind:RestartKind.Penalty);break;
        }
        if(kind is not ("free-kick" or "corner" or "penalty"))
        {
            // Remove unused actors from both AI and rendering for a readable drill.
            // These are disposable preview players, never career red cards.
            foreach(var p in m.Players)
            {
                bool needed=p.Keeper || p.Id==runner.Id || p.Id==defender.Id ||
                    ((kind is "pass" or "first-touch") && p.Id==7) ||
                    (kind=="cross" && p.Id==10);
                p.SentOff=!needed;
            }
        }
        m.Status="Football laboratory · "+kind;
        return m;
    }

    void GiveLabBall(Footballer runner)
    {
        Owner=runner.Id;Passer=-1;LastTouch=runner.Team;
        Ball=runner.Position+new Point(.31f,0);BallVelocity=new Point();BallHeight=0;
        runner.NextDribbleTouch=Seconds;runner.CarryLook=new Point(1,0);
    }

    void PrepareLabSetPiece()
    {
        foreach(var p in Players)
        {
            p.Position=World(p.Home,p.Team);
            p.Pace=4.7f+p.Profile.Acceleration*2;
            p.Decision=0;
        }
    }

    static void TuneLabPlayer(Footballer p,int rating)
    {
        float quality=Math.Clamp(rating/99f,.4f,1);
        p.Profile.Overall=rating;p.Skill=quality;p.Profile.Defence=quality;
        p.Pace=4.5f+quality*2.5f;
        p.Profile.StandingTackle=quality;p.Profile.SlidingTackle=quality;
        p.Profile.Interceptions=quality;p.Profile.Physical=quality;
        p.Profile.Endurance=.45f+quality*.5f;
        p.Profile.Aggression=.32f+quality*.5f;
        p.Profile.Passing=quality;p.Profile.Finishing=quality;
        p.Profile.Composure=quality;p.Profile.Vision=quality;
        p.Profile.Acceleration=quality;p.Profile.OffBall=quality;
        p.Profile.HeadingAccuracy=quality;p.Profile.Jumping=quality;
        p.Profile.FreeKickAccuracy=quality;p.Profile.Crossing=quality;
        p.Profile.GkDiving=quality;p.Profile.GkReflexes=quality;
        p.Profile.GkPositioning=quality;p.Profile.GkHandling=quality;
        p.Profile.SkillMoves=rating>=88?5:rating>=75?4:rating>=60?3:2;
    }
}