AttackMemory.cs

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