ai plan v2
This commit is contained in:
+335
-51
@@ -27,7 +27,10 @@ namespace AnotherReplayReader.Utils
|
||||
InvalidEvidenceLevel,
|
||||
UnitCapabilityContradiction,
|
||||
UnitTimelineContradiction,
|
||||
UnsupportedGameKnowledge
|
||||
UnsupportedGameKnowledge,
|
||||
OwnershipMissingEvidence,
|
||||
OwnershipConflict,
|
||||
OwnershipWeakConflict
|
||||
}
|
||||
|
||||
internal enum AIEvidenceLevel
|
||||
@@ -97,11 +100,13 @@ namespace AnotherReplayReader.Utils
|
||||
|
||||
public static AIValidationResult ValidateMachineReadableClaims(
|
||||
string response,
|
||||
ReplayFactIndex? factIndex = null)
|
||||
ReplayFactIndex? factIndex = null,
|
||||
IReadOnlyDictionary<string, int>? aiNameToPlayerIndex = null,
|
||||
StructuredKnowledge? structuredKnowledge = null)
|
||||
{
|
||||
var issues = ImmutableArray.CreateBuilder<AIValidationIssue>();
|
||||
var json = ExtractJsonObject(response);
|
||||
if (json is null || string.IsNullOrWhiteSpace(json))
|
||||
var jsonBlocks = ExtractAllJsonObjects(response);
|
||||
if (jsonBlocks.IsEmpty)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
@@ -110,7 +115,7 @@ namespace AnotherReplayReader.Utils
|
||||
return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable());
|
||||
}
|
||||
|
||||
var claims = ParseClaims(json, issues);
|
||||
var claims = ParseClaims(jsonBlocks, issues);
|
||||
if (claims is null)
|
||||
{
|
||||
return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable());
|
||||
@@ -120,7 +125,7 @@ namespace AnotherReplayReader.Utils
|
||||
ValidateUnpackAmbiguity(claims, issues);
|
||||
if (factIndex is not null)
|
||||
{
|
||||
ValidateTimelineConsistency(claims, factIndex, issues);
|
||||
ValidateTimelineConsistency(claims, factIndex, aiNameToPlayerIndex, structuredKnowledge, issues);
|
||||
}
|
||||
return new AIValidationResult(claims, issues.ToImmutable());
|
||||
}
|
||||
@@ -143,69 +148,112 @@ namespace AnotherReplayReader.Utils
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private static string? ExtractJsonObject(string response)
|
||||
private static ImmutableArray<string> ExtractAllJsonObjects(string response)
|
||||
{
|
||||
var result = ImmutableArray.CreateBuilder<string>();
|
||||
var markerIndex = response.LastIndexOf("[机器可读声明]", StringComparison.OrdinalIgnoreCase);
|
||||
var searchText = markerIndex >= 0 ? response.Substring(markerIndex) : response;
|
||||
|
||||
var matches = _jsonFenceRegex.Matches(searchText);
|
||||
if (matches.Count > 0)
|
||||
{
|
||||
return matches[matches.Count - 1].Groups[1].Value;
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
result.Add(match.Groups[1].Value);
|
||||
}
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
// 回退:整段响应中所有 fenced JSON
|
||||
var allMatches = _jsonFenceRegex.Matches(response);
|
||||
if (allMatches.Count > 0)
|
||||
{
|
||||
foreach (Match match in allMatches)
|
||||
{
|
||||
result.Add(match.Groups[1].Value);
|
||||
}
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
var start = searchText.LastIndexOf('{');
|
||||
var end = searchText.LastIndexOf('}');
|
||||
if (start >= 0 && end > start)
|
||||
{
|
||||
return searchText.Substring(start, end - start + 1);
|
||||
result.Add(searchText.Substring(start, end - start + 1));
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
return null;
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
private static AIMachineReadableClaims? ParseClaims(
|
||||
string json,
|
||||
ImmutableArray<string> jsonBlocks,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json, new JsonDocumentOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
});
|
||||
var mergedUnits = new Dictionary<string, AIUnitClaim>(StringComparer.OrdinalIgnoreCase);
|
||||
var mergedEvents = new List<AIEventClaim>();
|
||||
var mergedTimelines = new List<AITimelineClaim>();
|
||||
var parsedAny = false;
|
||||
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
foreach (var json in jsonBlocks)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json, new JsonDocumentOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
});
|
||||
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
"机器可读声明不是 JSON object。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedAny = true;
|
||||
foreach (var unitClaim in ReadUnitClaims(root, issues))
|
||||
{
|
||||
mergedUnits[unitClaim.UnitId] = unitClaim;
|
||||
}
|
||||
foreach (var claim in ReadSimpleClaims(root, "eventClaims", issues)
|
||||
.Select(c => new AIEventClaim(c.Claim, c.EvidenceLevel, c.Evidence)))
|
||||
{
|
||||
mergedEvents.Add(claim);
|
||||
}
|
||||
foreach (var claim in ReadSimpleClaims(root, "timelineClaims", issues)
|
||||
.Select(c => new AITimelineClaim(c.Claim, c.EvidenceLevel, c.Evidence)))
|
||||
{
|
||||
mergedTimelines.Add(claim);
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
"机器可读声明不是 JSON object。"));
|
||||
return null;
|
||||
$"机器可读声明 JSON 解析失败:{ex.Message}"));
|
||||
}
|
||||
|
||||
return new AIMachineReadableClaims(
|
||||
ReadUnitClaims(root, issues),
|
||||
ReadSimpleClaims(root, "eventClaims", issues)
|
||||
.Select(c => new AIEventClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||
.ToImmutableArray(),
|
||||
ReadSimpleClaims(root, "timelineClaims", issues)
|
||||
.Select(c => new AITimelineClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||
.ToImmutableArray());
|
||||
}
|
||||
catch (JsonException ex)
|
||||
|
||||
if (!parsedAny)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"机器可读声明 JSON 解析失败:{ex.Message}"));
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AIMachineReadableClaims(
|
||||
mergedUnits.Values.Take(MaxUnitClaims).ToImmutableArray(),
|
||||
mergedEvents.Take(MaxEventClaims).ToImmutableArray(),
|
||||
mergedTimelines.Take(MaxTimelineClaims).ToImmutableArray());
|
||||
}
|
||||
|
||||
private const int MaxUnitClaims = 10;
|
||||
private const int MaxEventClaims = 5;
|
||||
private const int MaxTimelineClaims = 3;
|
||||
|
||||
private static ImmutableArray<AIUnitClaim> ReadUnitClaims(JsonElement root, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
@@ -263,12 +311,12 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
var result = ImmutableArray.CreateBuilder<SimpleClaim>();
|
||||
var maxClaims = propertyName switch
|
||||
{
|
||||
"eventClaims" => 5,
|
||||
"timelineClaims" => 3,
|
||||
_ => 50,
|
||||
};
|
||||
var maxClaims = propertyName switch
|
||||
{
|
||||
"eventClaims" => MaxEventClaims,
|
||||
"timelineClaims" => MaxTimelineClaims,
|
||||
_ => 50,
|
||||
};
|
||||
var totalCount = 0;
|
||||
foreach (var item in claims.EnumerateArray())
|
||||
{
|
||||
@@ -360,6 +408,7 @@ namespace AnotherReplayReader.Utils
|
||||
Select,
|
||||
Move,
|
||||
Power,
|
||||
Protocol,
|
||||
Unknown
|
||||
}
|
||||
|
||||
@@ -372,6 +421,9 @@ namespace AnotherReplayReader.Utils
|
||||
public string? GetSpecialPowerName() =>
|
||||
Type == AIEvidenceType.Power && Parameters.Length >= 1 ? Parameters[0] : null;
|
||||
|
||||
public string? GetTechName() =>
|
||||
Type == AIEvidenceType.Protocol && Parameters.Length >= 1 ? Parameters[0] : null;
|
||||
|
||||
public string? GetUnitId() => Type switch
|
||||
{
|
||||
AIEvidenceType.Build or AIEvidenceType.Place when Parameters.Length >= 2 => Parameters[1],
|
||||
@@ -391,7 +443,7 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
private static readonly Regex _structuredEvidenceRegex = new(
|
||||
@"^(build|place|produce|sell|select|move|power)\|([^|]+(?:\|(?!\|).*)?)$",
|
||||
@"^(build|place|produce|sell|select|move|power|protocol)\|([^|]+(?:\|(?!\|).*)?)$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
internal static StructuredEvidence ParseStructuredEvidence(string text)
|
||||
@@ -411,6 +463,7 @@ namespace AnotherReplayReader.Utils
|
||||
"select" => AIEvidenceType.Select,
|
||||
"move" => AIEvidenceType.Move,
|
||||
"power" => AIEvidenceType.Power,
|
||||
"protocol" => AIEvidenceType.Protocol,
|
||||
_ => AIEvidenceType.Unknown,
|
||||
};
|
||||
|
||||
@@ -472,6 +525,8 @@ namespace AnotherReplayReader.Utils
|
||||
private static void ValidateTimelineConsistency(
|
||||
AIMachineReadableClaims claims,
|
||||
ReplayFactIndex factIndex,
|
||||
IReadOnlyDictionary<string, int>? aiNameToPlayerIndex,
|
||||
StructuredKnowledge? structuredKnowledge,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
foreach (var claim in claims.UnitClaims)
|
||||
@@ -496,10 +551,37 @@ namespace AnotherReplayReader.Utils
|
||||
continue;
|
||||
}
|
||||
|
||||
var playerIndex = TryParsePlayerIndex(claim.Player, aiNameToPlayerIndex, out var parsed)
|
||||
? parsed
|
||||
: -1;
|
||||
if (playerIndex < 0)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
"Unit claim 缺少可识别的 player,无法校验所有权与生产时间线。",
|
||||
claim.UnitId));
|
||||
}
|
||||
|
||||
// Check 2: Verify special power evidence against fact index
|
||||
var evidence = ParseAllEvidence(claim.Evidence);
|
||||
foreach (var ev in evidence)
|
||||
{
|
||||
if (ev.Type == AIEvidenceType.Protocol && playerIndex >= 0)
|
||||
{
|
||||
var techName = ev.GetTechName();
|
||||
if (techName is not null
|
||||
&& factIndex.PlayerTechChoices.TryGetValue(playerIndex, out var chosen)
|
||||
&& !chosen.Contains(techName))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Contradiction,
|
||||
AIValidationIssueKind.UnitCapabilityContradiction,
|
||||
$"玩家 {claim.Player} 在回放中选择过的协议为:{string.Join(", ", chosen.OrderBy(x => x))},但 AI 声称其选择了“{techName}”——该协议未观察到。",
|
||||
claim.UnitId));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ev.Type != AIEvidenceType.Power)
|
||||
{
|
||||
continue;
|
||||
@@ -527,20 +609,30 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
// Did this UnitId actually use this special power?
|
||||
if (factIndex.UnitIdSpecialPowers.TryGetValue(evUnitId, out var actualPowers)
|
||||
&& !actualPowers.Contains(powerName))
|
||||
if (factIndex.UnitIdSpecialPowers.TryGetValue(evUnitId, out var actualPowers))
|
||||
{
|
||||
if (!actualPowers.Contains(powerName))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Contradiction,
|
||||
AIValidationIssueKind.UnitCapabilityContradiction,
|
||||
$"UnitId {evUnitIdStr} 在回放中使用过以下特殊能力:{string.Join(", ", actualPowers.OrderBy(x => x))},但 AI 声称其使用了“{powerName}”——此能力未在该 UnitId 上观察到。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Contradiction,
|
||||
AIValidationIssueKind.UnitCapabilityContradiction,
|
||||
$"UnitId {evUnitIdStr} 在回放中使用过以下特殊能力:{string.Join(", ", actualPowers.OrderBy(x => x))},但 AI 声称其使用了“{powerName}”——此能力未在该 UnitId 上观察到。",
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.WeakEvidence,
|
||||
$"UnitId {evUnitIdStr} 在回放中未观察到任何特殊能力,但 AI 声称其使用了“{powerName}”,证据不足。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: UnitId used as builder vs claim
|
||||
var isBuilderInReplay = factIndex.BuilderUnitIds.Contains(unitId);
|
||||
var claimLooksLikeBuilder = ClaimLooksLikeBuilder(claim.Claim);
|
||||
var claimLooksLikeBuilder = ClaimLooksLikeBuilder(claim.Claim, structuredKnowledge);
|
||||
if (claimLooksLikeBuilder && !isBuilderInReplay)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
@@ -549,9 +641,199 @@ namespace AnotherReplayReader.Utils
|
||||
$"AI 推测 UnitId {claim.UnitId} 是“{claim.Claim}”(推测是建造单位),但该 UnitId 在回放中从未作为建造者(建造建筑)出现。",
|
||||
claim.UnitId));
|
||||
}
|
||||
|
||||
// Check 4: 所有权证据(强/弱分层)
|
||||
if (playerIndex >= 0)
|
||||
{
|
||||
ValidateOwnership(claim, unitId, playerIndex, factIndex, issues);
|
||||
}
|
||||
|
||||
// Check 5: move 证据不能单独支撑高置信
|
||||
if (claim.EvidenceLevel is AIEvidenceLevel.Confirmed or AIEvidenceLevel.HighlyLikely
|
||||
&& !claim.Evidence.IsEmpty)
|
||||
{
|
||||
var parsedEvidence = ParseAllEvidence(claim.Evidence);
|
||||
if (parsedEvidence.All(e => e.Type == AIEvidenceType.Move))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.WeakEvidence,
|
||||
$"UnitId {claim.UnitId} 的高置信推测只使用 move 证据(不携带 UnitId,无法验证),建议降级或补充其他证据。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
|
||||
// Check 6: 首次出兵时间线(轰炸机类)
|
||||
if (playerIndex >= 0)
|
||||
{
|
||||
ValidateFirstProductionTimeline(claim, unitId, playerIndex, factIndex, structuredKnowledge, issues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateOwnership(
|
||||
AIUnitClaim claim,
|
||||
uint unitId,
|
||||
int playerIndex,
|
||||
ReplayFactIndex factIndex,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
var myStrong = factIndex.PlayerStrongOwnershipUnitIds.TryGetValue(playerIndex, out var strong)
|
||||
&& strong.Contains(unitId);
|
||||
var myWeak = factIndex.PlayerWeakOwnershipUnitIds.TryGetValue(playerIndex, out var weak)
|
||||
&& weak.Contains(unitId);
|
||||
|
||||
// 规则 1:高置信但无任何所有权证据
|
||||
if (claim.EvidenceLevel is AIEvidenceLevel.Confirmed or AIEvidenceLevel.HighlyLikely
|
||||
&& !myStrong && !myWeak)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.OwnershipMissingEvidence,
|
||||
$"UnitId {claim.UnitId} 声称属于玩家 {claim.Player},但回放中该玩家对它的强/弱所有权证据(编队、建造者、维修、出售、施法者、选择)均未观察到,建议降级。",
|
||||
claim.UnitId));
|
||||
}
|
||||
|
||||
// 规则 2/4:其他玩家的强证据 → 冲突
|
||||
foreach (var kv in factIndex.PlayerStrongOwnershipUnitIds)
|
||||
{
|
||||
if (kv.Key == playerIndex || !kv.Value.Contains(unitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var message = myStrong
|
||||
? $"UnitId {claim.UnitId} 同时存在玩家 {claim.Player} 与玩家 {kv.Key} 的强所有权证据(异常/作弊操作),无法判定归属。"
|
||||
: $"UnitId {claim.UnitId} 声称属于玩家 {claim.Player},但玩家 {kv.Key} 对它有强所有权证据(编队/建造者/维修/出售/施法者)。";
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Contradiction,
|
||||
AIValidationIssueKind.OwnershipConflict,
|
||||
message,
|
||||
claim.UnitId));
|
||||
break;
|
||||
}
|
||||
|
||||
// 规则 3:仅其他玩家的弱证据,且己方无证据 → 警告
|
||||
if (!myStrong && !myWeak)
|
||||
{
|
||||
foreach (var kv in factIndex.PlayerWeakOwnershipUnitIds)
|
||||
{
|
||||
if (kv.Key == playerIndex || !kv.Value.Contains(unitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.OwnershipWeakConflict,
|
||||
$"UnitId {claim.UnitId} 声称属于玩家 {claim.Player},但玩家 {kv.Key} 选中过它(可能只是点击敌方单位查看血量,不能完全排除),建议降置信度。",
|
||||
claim.UnitId));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateFirstProductionTimeline(
|
||||
AIUnitClaim claim,
|
||||
uint unitId,
|
||||
int playerIndex,
|
||||
ReplayFactIndex factIndex,
|
||||
StructuredKnowledge? structuredKnowledge,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
if (claim.EvidenceLevel is not (AIEvidenceLevel.Confirmed or AIEvidenceLevel.HighlyLikely))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!factIndex.UnitIdFirstObservedTime.TryGetValue(unitId, out var firstObserved))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!factIndex.PlayerFirstProductionTime.TryGetValue(playerIndex, out var productions)
|
||||
|| productions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var claimedBombers = GetBomberAssetNames(claim.Claim, structuredKnowledge);
|
||||
if (claimedBombers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var wildcard = claimedBombers.Contains("*");
|
||||
var relevant = productions
|
||||
.Where(kv => claimedBombers.Contains(kv.Key)
|
||||
|| (wildcard && IsBomberLikeName(kv.Key)))
|
||||
.Select(kv => kv.Value)
|
||||
.ToList();
|
||||
if (relevant.Count == 0 || !relevant.All(t => t > firstObserved))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var firstTime = $"{(int)firstObserved.TotalMinutes}:{firstObserved:ss\\.ff}";
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Contradiction,
|
||||
AIValidationIssueKind.UnitTimelineContradiction,
|
||||
$"UnitId {claim.UnitId} 首次出现于 {firstTime},早于玩家 {claim.Player} 首次生产轰炸机类单位的时间,声称其为“{claim.Claim}”与回放时间线矛盾。",
|
||||
claim.UnitId));
|
||||
}
|
||||
|
||||
private static HashSet<string> GetBomberAssetNames(
|
||||
string claimText,
|
||||
StructuredKnowledge? structuredKnowledge)
|
||||
{
|
||||
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var structured = structuredKnowledge ?? StructuredKnowledge.Instance;
|
||||
if (structured is not null)
|
||||
{
|
||||
foreach (var entity in structured.EntitiesWithTag("bomber"))
|
||||
{
|
||||
if (claimText.IndexOf(entity.AssetName, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
result.Add(entity.AssetName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.Count == 0
|
||||
&& (claimText.IndexOf("轰炸机", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claimText.IndexOf("bomber", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
result.Add("*");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsBomberLikeName(string assetName) =>
|
||||
assetName.IndexOf("Bomber", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| assetName.IndexOf("轰炸", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
private static bool TryParsePlayerIndex(
|
||||
string playerText,
|
||||
IReadOnlyDictionary<string, int>? aiNameToPlayerIndex,
|
||||
out int playerIndex)
|
||||
{
|
||||
playerIndex = -1;
|
||||
if (string.IsNullOrWhiteSpace(playerText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var trimmed = playerText.Trim();
|
||||
if (aiNameToPlayerIndex is not null && aiNameToPlayerIndex.TryGetValue(trimmed, out playerIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (trimmed.StartsWith("Player", StringComparison.OrdinalIgnoreCase)
|
||||
&& int.TryParse(trimmed.Substring("Player".Length), out playerIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (trimmed.StartsWith("AI_", StringComparison.OrdinalIgnoreCase)
|
||||
&& int.TryParse(trimmed.Substring("AI_".Length), out playerIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryGetArray(JsonElement root, string propertyName, out JsonElement array)
|
||||
@@ -652,10 +934,12 @@ namespace AnotherReplayReader.Utils
|
||||
/// Uses structured knowledge (knowledge_units.json) when available, falls
|
||||
/// back to heuristic string matching for backward compatibility.
|
||||
/// </summary>
|
||||
private static bool ClaimLooksLikeBuilder(string claimText)
|
||||
private static bool ClaimLooksLikeBuilder(
|
||||
string claimText,
|
||||
StructuredKnowledge? structuredKnowledge)
|
||||
{
|
||||
// Primary: structured knowledge lookup
|
||||
var structured = StructuredKnowledge.Instance;
|
||||
var structured = structuredKnowledge ?? StructuredKnowledge.Instance;
|
||||
if (structured is not null)
|
||||
{
|
||||
foreach (var entity in structured.AllEntities)
|
||||
|
||||
Reference in New Issue
Block a user