Bot/BotStats.cs
using System;
using System.Collections.Generic;
using System.Text;
/// <summary>
/// bot 行为统计(v0.7.8.95,调参用):host 侧按球 Id 记各行为的触发计数,
/// 编辑器 MCP `cr_botstats` 读取。纯内存、不联网;热重载会清零(可接受)。
/// 目的:调 bot 数值有数据可看(逃跑/追猎/喂人/喂旗/技能的分布),不靠肉眼猜。
/// </summary>
public static class BotStats
{
/// <summary> 一只 bot 的累计计数(Difficulty/Archetype 存 byte 避免游戏程序集类型出现在键上) </summary>
public sealed class Entry
{
public string Name = "";
public int Team = -1;
public byte Difficulty;
public byte Archetype;
public TimeSince Seen; // 最后一次决策时刻(30s 不活跃的条目不再输出)
public long Decisions, Flees, Kites, Bequests, Hunts, SplitEscapes, SplitHunts, FeedMates, FeedFlags, SpikeBombs, Skills;
}
static readonly Dictionary<Guid, Entry> _byId = new();
/// <summary> 条目数上限:球对象会被销毁重建(菜单演示赛收场、回菜单清场),而 _byId 按 Ball.Id
/// 累积、条目永不失效——长会话会把"这台机器见过的所有 bot 球"都留在字典里。超过上限时
/// 清掉久未决策(Seen > PruneAfterSeconds)的条目(v0.7.8.105)。 </summary>
const int MaxEntries = 256;
const float PruneAfterSeconds = 60f;
/// <summary> 回收久未决策的条目(Track 内按需触发,不做每帧扫描) </summary>
static readonly List<Guid> _pruneScratch = new();
static void PruneStale()
{
if ( _byId.Count <= MaxEntries ) return;
_pruneScratch.Clear();
foreach ( var kv in _byId )
{
if ( kv.Value.Seen > PruneAfterSeconds ) _pruneScratch.Add( kv.Key );
}
foreach ( var k in _pruneScratch ) _byId.Remove( k );
}
/// <summary> 取/建一只 bot 的计数条目(BotBrain 每次决策调一次,顺带刷新名字/队伍/活跃时刻) </summary>
public static Entry Track( Ball ball, byte difficulty, byte archetype )
{
if ( ball is null || !ball.IsValid() ) return null;
PruneStale(); // 超上限时清久未决策的条目,防字典随"见过的 bot 球总数"无界增长
if ( !_byId.TryGetValue( ball.Id, out var e ) )
{
e = new Entry();
_byId[ball.Id] = e;
}
e.Name = ball.PlayerName;
e.Team = ball.TeamIndex;
e.Difficulty = difficulty;
e.Archetype = archetype;
e.Seen = 0;
return e;
}
public static string Dump()
{
// 只输出 30 秒内还活着的 bot——跨局残留的旧球条目不再刷屏
if ( _byId.Count == 0 ) return "no bot stats yet";
var sb = new StringBuilder();
int shown = 0;
sb.AppendLine( "name | team | diff | archetype | dec | flee | kite | bequest | hunt | spEsc | spHunt | feed | feedFlag | bomb | skill" );
foreach ( var e in _byId.Values )
{
if ( e.Seen > 30f ) continue;
shown++;
sb.AppendLine(
$"{e.Name} | T{e.Team} | {(BotDifficulty)e.Difficulty} | {(BotArchetype)e.Archetype} | " +
$"{e.Decisions} | {e.Flees} | {e.Kites} | {e.Bequests} | {e.Hunts} | {e.SplitEscapes} | {e.SplitHunts} | " +
$"{e.FeedMates} | {e.FeedFlags} | {e.SpikeBombs} | {e.Skills}" );
}
sb.Insert( 0, $"bots={shown} (of {_byId.Count} tracked)\n" );
return sb.ToString();
}
}