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.
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;
}
}