Partial MatchSimulation class that picks and starts AI strategy plans automatically. Scores each stored plan against the ball carrier's position, team shape and passing lanes, then chooses one by weighted random pick with a per-plan reuse cooldown and logs the start event.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Socer;
public sealed partial class MatchSimulation
{
public bool AutomaticStrategiesEnabled=true;
public List<StrategyPlan> AutomaticStrategies {get;private set;}=new();
public int AutomaticStrategyStarts;
public float AutomaticStrategyReadyAt;
float nextAutomaticStrategyCheck;
readonly Dictionary<string,float> strategyLastUsed=new();
public void SetAutomaticStrategies(IEnumerable<StrategyPlan> plans)
=>AutomaticStrategies=plans.Where(p=>p!=null && p.Validate()=="").Select(p=>p.Copy()).ToList();
bool HasPersonalOrder(Footballer p)=>p.DirectOrder!=DirectOrderKind.None && Seconds<p.DirectOrderUntil;
float AutomaticStrategyFit(StrategyPlan plan,Footballer carrier)
{
if(!plan.Phases.Any(f=>f.Action!=2))return 0;
var first=plan.Phases[0];var anchor=World(carrier.Position,carrier.Team);
var offset=anchor-plan.Start[first.Actor].Point();
// Preserve the intended part of the pitch and flank. Do not translate a wing
// routine into our penalty area or clamp its runs into the touchline.
if(anchor.X<38 || Math.Abs(offset.X)>24 || Math.Abs(offset.Y)>14)return 0;
bool Inside(Point v)=>v.X>=1 && v.X<=104 && v.Y>=1 && v.Y<=67;
if(plan.Start.Any(s=>!Inside(s.Point()+offset)) || plan.Phases.Any(f=>
(f.PassZone!=null && !Inside(f.PassZone.Point()+offset)) ||
f.Routes.Any(r=>r.Points.Any(s=>!Inside(s.Point()+offset)))))return 0;
if(!BindStrategyActors(plan,carrier.Team,false,out var actors,out _))return 0;
float total=0;
for(int role=0;role<5;role++)
{
var p=Players[actors[role]];
if(HasPersonalOrder(p) || p.Stamina<.25f || p.Recovery>.1f)return 0;
float mismatch=(World(p.Position,p.Team)-plan.Start[role].Point()-offset).Length;
if(mismatch>24)return 0;total+=mismatch;
}
if(total/5>13)return 0;
if(first.Action==0)
{
var receiver=Players[actors[first.Receiver]];
var target=first.PassZone!=null?World(first.PassZone.Point()+offset,carrier.Team):receiver.Position;
if(!SeesPartner(carrier,receiver) || !IsOnside(receiver,carrier.Position) ||
(target-carrier.Position).Length>40 || PerceivedLane(carrier,target)<(first.Delivery==2?.5f:1.4f))return 0;
}
if(first.Action==1 && ((World(new Point(105,34),carrier.Team)-carrier.Position).Length>28 ||
PerceivedLane(carrier,World(new Point(105,34),carrier.Team),true)<1))return 0;
return Math.Clamp(1-total/85-Math.Abs(offset.X)/100-Math.Abs(offset.Y)/70,.15f,1);
}
bool TryAutomaticStrategy(Footballer carrier)
{
if(!AutomaticStrategiesEnabled || AutomaticStrategies.Count==0 || StrategyWalkthrough ||
carrier.Team!=0 || Owner!=carrier.Id || carrier.Keeper || carrier.Held<.45f || !BallAtFoot(carrier) ||
Finished || RestartRemaining>0 || IncidentRemaining>0 || GoalHoldRemaining>0 || WindupPlayer>=0 ||
(Strategy!=null && !Strategy.Done) || HasPersonalOrder(carrier) ||
Seconds<AutomaticStrategyReadyAt || Seconds<nextAutomaticStrategyCheck)return false;
nextAutomaticStrategyCheck=Seconds+2.5f+Next()*2.5f;
if(CarrierPressure(carrier)<2 || Next()>.3f+carrier.Profile.Vision*.22f)return false;
var options=AutomaticStrategies.Select(plan=>new {Plan=plan,Fit=AutomaticStrategyFit(plan,carrier)})
.Where(x=>x.Fit>0).Select(x=>new {x.Plan,Weight=x.Fit*(strategyLastUsed.TryGetValue(x.Plan.Name,out var last) && Seconds-last<75?.22f:1)})
.ToArray();
float total=options.Sum(x=>x.Weight);if(total<=0)return false;
float pick=Next()*total;var chosen=options[^1].Plan;
foreach(var option in options){pick-=option.Weight;if(pick<=0){chosen=option.Plan;break;}}
if(!StartStrategy(chosen))return false;
AutomaticStrategyStarts++;
AutomaticStrategyReadyAt=Seconds+24+Next()*12;
Strategy.Status="Авто · "+chosen.Name;
Strategy.Events.Add("Инициатива игроков");
Status=TeamName(0)+" · разыгрывают «"+chosen.Name+"»";
AddEvent(Status);Say(carrier,"По схеме!",true);
return true;
}
}