Per-player memory that records opposing players' visible pass and dribble cues, plus partial MatchSimulation helpers that use that memory to shift a defender's marking target and to estimate pass risk.
using System;
using System.Collections.Generic;
namespace Socer;
public enum AttackCue { DribbleLeft, DribbleRight, PassLeft, PassCentre, PassRight }
// Per defender / attacker, learned only from nearby visible actions. No future intent or RNG.
public sealed class AttackMemory
{
readonly float[,,] evidence=new float[22,22,5];
readonly float[,,] observedAt=new float[22,22,5];
public int Observations { get; private set; }
public float Recognition(int observer,int attacker,AttackCue cue,float now)
{
if(observer<0 || observer>=22 || attacker<0 || attacker>=22)return 0;
int c=(int)cue;
float count=evidence[observer,attacker,c]*MathF.Exp(-Math.Max(0,now-observedAt[observer,attacker,c])/65);
return Math.Clamp((count-1)/4,0,.8f);
}
public void Observe(IReadOnlyList<Footballer> players,Footballer actor,AttackCue cue,float now)
{
foreach(var watcher in players)
{
if(watcher.Team==actor.Team || watcher.SentOff || now<watcher.FallUntil)continue;
var delta=actor.Position-watcher.Position;
if(delta.Length>28 || (delta.Length>6 && Point.Dot(delta.Unit,new(MathF.Cos(watcher.Facing),MathF.Sin(watcher.Facing)))<-.2f))continue;
int c=(int)cue;
float prior=evidence[watcher.Id,actor.Id,c]*MathF.Exp(-Math.Max(0,now-observedAt[watcher.Id,actor.Id,c])/65);
evidence[watcher.Id,actor.Id,c]=Math.Min(6,prior+.65f+watcher.Profile.Vision*.35f);
observedAt[watcher.Id,actor.Id,c]=now;Observations++;
}
}
public static AttackCue PassCue(Point from,Point to)=>to.Y<from.Y-4?AttackCue.PassLeft:to.Y>from.Y+4?AttackCue.PassRight:AttackCue.PassCentre;
}
public sealed partial class MatchSimulation
{
public AttackMemory OpponentMemory { get; }=new();
public int AdaptiveCovers,AdaptiveVariations;
Point AdaptDefence(Footballer defender,Point target)
{
if(defender.Keeper || Owner<0 || Players[Owner].Team==defender.Team || WindupPlayer>=0)return target;
var attacker=Players[Owner];
if((defender.Position-attacker.Position).Length>18 || (defender.Id!=secondChasers[defender.Team]))return target;
AttackCue best=AttackCue.PassCentre;float confidence=0;
for(int c=2;c<5;c++)
{
float known=OpponentMemory.Recognition(defender.Id,attacker.Id,(AttackCue)c,Seconds);
if(known>confidence){confidence=known;best=(AttackCue)c;}
}
if(confidence<.18f)return target;
var lane=new Point(Direction(attacker.Team)*4,best==AttackCue.PassLeft?-5:best==AttackCue.PassRight?5:0);
defender.Action="Читает повторный пас";AdaptiveCovers++;
return target*(1-confidence*.65f)+Clamp(attacker.Position+lane)*(confidence*.65f);
}
float FamiliarPassRisk(Footballer passer,Point target)
{
float risk=0;var cue=AttackMemory.PassCue(passer.Position,target);
foreach(var defender in ActivePlayers)
if(defender.Team!=passer.Team && (defender.Position-passer.Position).Length<18)
risk=Math.Max(risk,OpponentMemory.Recognition(defender.Id,passer.Id,cue,Seconds));
return risk;
}
}